Search code examples
javabukkitspigot

java get random from list by its percentage chance


i've have an list:

public List<Map<Integer, Rarity>> classes = Arrays.asList(
  Collections.singletonMap(0, new Rarity("rarity_default", "Default!", 55.0)),
  // ...
);

A 55.0 is a 55% chance in this list.

I want to get a random Rarity class from this classes but get it by a chance of it.

For example i got these chances:

  • rarity_A: 55.00%
  • rarity_B: 35.00%
  • rarity_C: 12.00%
  • rarity_D: 6.00%
  • rarity_E: 3.00%
  • rarity_F: 0.10%

Also I don't want it to get a item always like I've tried to code it myself but it worked up to 12.00% and below it won't give me the rarity. And also it would give me the rarity with highest chance instead of nothing because I don't want to always get it.

Currently here is my code:

  private final Map<Integer, Rarity> classes = new HashMap<>();

  private final Random random = new Random();

  public MobKillListener() {
    for (Map<Integer, Rarity> rarityMap : SchemePlugin.getInstance().getSchemeConfig().classes) {
      classes.putAll(rarityMap);
    }
  }

  @EventHandler
  public void onMobKill(EntityDeathEvent event) {
    if (!(event.getEntity().getKiller() instanceof Player))
      return;

    Map<Rarity, Double> rarities = new HashMap<>();

    for (Map.Entry<Integer, Rarity> entry : classes.entrySet()) {
      rarities.put(entry.getValue(), entry.getValue().rarity); // i need to remake the map
    }
    
    Rarity rarity = random(rarities);
    if (rarity != null) {
      // do my stuff
    }
  }

  // i don't want to see this function ever again, this made me so mad..
  public <T> T random(Map<T, Double> map) {
    double rand = (double) new Random().nextInt(10000001) / 100000.0D;
    double total = 0.0D;

    for (T t : map.keySet()) {
      double chance = map.get(t) / 4.0D;
      total += chance;

      if (total >= rand) {
        System.out.println("(" + chance + ") " + "Got " + total + ", rand: " + rand);
        return t;
      }
    }
    System.out.println("Got " + total + ", rand: " + rand);

    return null;
  }

Yes im doing this for Spigot plugin.


Solution

  • If I'm understanding correctly, you're just trying to get a weighted random value from a list of entries? If so, there are two different ways you can do this.

    Option 1

    Use the library Apache Commons Math3. It's been awhile since I've done things in the Spigot space but I think it's included. If it's not, just add it as a bundled dependency in your pom.xml file if you're using Maven or your build.gradle if you're using Gradle.

    To implement the code, you would do something similar to the following:

    private final EnumeratedDistribution<Rarity> rarityDistribution;
    
    public MobKillListener() {
      // List used to construct the EnumeratedDistribution
      List<Pair<Rarity, Double>> distributionItems = new ArrayList<>();
      // Iterate over each of the maps in the SchemeConfig
      for (Map<Integer, Rarity> rarityMap : SchemePlugin.getInstance().getSchemeConfig().classes) {
        // Iterate over each entry within the SchemeConfig maps
        for (Map.Entry<Integer, Rarity> entry : rarityMap.entrySet()) {
          // Add a Pair to the items where the key is the Rarity and the value is the Rarity's value
          distributionItems.add(new Pair<>(entry.getValue(), entry.getValue().rarity));
        }
      }
    
      this.rarityDistribution = new EnumeratedDistribution(distributionItems);
    }
    
    
    public Rarity randomRarity() {
      return this.rarityDistribution.sample();
    }
    

    With this example, all you'd need to do to add an entry that does not result in a value is add in something like this in the constructor, replacing the 0.3 with whatever value you'd like for the probability:

    distributionItems.add(new Pair<>(null, 0.3));
    

    Option 2

    This is a less ideal option because you'd be implementing the "picker" yourself and will likely be less performant (I did not look into the inner workings of EnumeratedDistribution to confirm this so grain of salt):

    public class WeightedRandomPicker<T> {
      private final NavigableMap<Double, T> map = new TreeMap<>();
      private final Random random;
      private double total = 0;
    
      // constructor using the Java default Random implementation
      public WeightedRandomPicker() {
        this(new Random());
      }
    
      // constructor allowing an arbitrary Random implementation
      public WeightedRandomPicker(Random random) {
        this.random = random;
      }
    
      public void add(double weight, T entry) {
        if (weight <= 0)
          return;
    
        // update the total value, allowing things to not add up to 100 or 1.00
        total += weight;
        // add the new entry to the internal Map
        // VERY IMPORTANT THAT YOU USE `total` AND NOT WEIGHT HERE
        map.put(total, result);
      }
    
      public T randomEntry() {
        double randomWeight = this.random.nextDouble() * total;
        return map.ceilingEntry(value).getValue();
      }
    }
    

    After you have that, you'd implement your MobKillerListener similarly to Option 1:

    private final WeightedRandomPicker<Rarity> randomPicker = new WeightedRandomPicker<>();
    
    public MobKillListener() {
      // Iterate over each of the maps in the SchemeConfig
      for (Map<Integer, Rarity> rarityMap : SchemePlugin.getInstance().getSchemeConfig().classes) {
        // Iterate over each entry within the SchemeConfig maps
        for (Map.Entry<Integer, Rarity> entry : rarityMap.entrySet()) {
          // Add the entry with the specified weight to the WeightedRandomPicker
          randomPicker.add(entry.getValue().rarity, entry.getValue());
        }
      }
    }
    
    @EventHandler
    public void onMobKill(EntityDeathEvent event) {
      if (!(event.getEntity().getKiller() instanceof Player))
        return;
        
      Rarity rarity = random(this.randomPicker);
      if (rarity != null) {
        // do my stuff
      }
    }
    
    public <T> T randomRarity(WeightedRandomPicker<T> randomPicker) {
      return randomPicker.randomEntry();
    }
    

    To add an entry that does not result in a value, you can use the following code, placed at the end of the constructor, replacing 0.3 with whatever weight you'd like:

    randomPicker.add(0.3, null);
    

    Conclusion

    I'd encourage you going forward to try to figure out the best way to express your question abstractly and then check Google first. Although I wrote the below code, you could find countless other examples online by searching "java weighted random from list".

    Either way, I'd encourage you to use Option 1 since it requires less code that you need to maintain, but Option 2 should work perfectly fine if you don't like the idea of including a third-party library.