0

I would like to be able to return (and in our case, for the example, display) an item of an enumeration, according to a certain predefined percentage of chance for each item. An illustration in code of this enumeration just below :

package me.lucas.test;

public enum Item {

    ITEM_1("Item 1", 30),
    ITEM_2("Item 2", 30),
    ITEM_3("Item 3", 10),
    ITEM_4("Item 4", 65);

    private final String name;
    private final int percentage;

    Item(String name, int percentage) {
        this.name = name;
        this.percentage = percentage;
    }

    public String getName() {
        return name;
    }

    public int getPercentage() {
        return percentage;
    }

}

Also, these items will be stored in a List.

package me.lucas.test;

import java.util.ArrayList;
import java.util.List;

public class Program {

    private final List<Item> items = new ArrayList<>();

    /**
     * Displays an item among those in the list, according to their percentage chance of appearing.
     * @param items The items
     */
    public void displayItem(List<Item> items) {
        Item item;

        item = ?

        System.out.println("Selected item : " + item.getName() + " with percentage : " + item.getPercentage());
    }

}

Namely that, as in this example, two items can have the same percentage of chance to appear.

I also looked at the following topic (Java Random Percentage Chance) but in my case, the percentages are not predefined in the expressions directly but in the enumeration.

So I would appreciate your help. Thank you in advance for taking the time to help me.

  • When running this example, you want to display something or not, based on a chance, the same chance, for all itens in the list, for that execution? Is that it? – Bonatti Nov 05 '20 at 20:06
  • 2
    Note that your total percentage is more than 1 (it is 135%) – knittl Nov 05 '20 at 20:07
  • @knittl I think that the chances are true/false, they are independant, not to be used togheter – Bonatti Nov 05 '20 at 20:08
  • 1
    This is not a homework-writing service. And 135% is just as easy as 100% to figure out. – NomadMaker Nov 05 '20 at 20:09
  • You will need to know the sum of the percentage of the items (because I think it can be not 100). But the percentage field in `Item` is private. – Omid.N Nov 05 '20 at 20:09
  • @NomadMaker Did you notice the `New contributor` label? Perhaps a more helpful approach is in order. – WJS Nov 05 '20 at 20:10
  • @Bonatti In this exemple, the ITEM_1 has 30% of chance to be selected, ITEM_3 has 10% of chance, etc. And knittl I know, but this is intentional. I would like each item to have a predefined percentage to appear, even if for the total of the items it exceeds 100%. This is not achievable? – lucasparmentier388 Nov 05 '20 at 20:11
  • @WJS If he had more than more experience, I would have just state the homework policy. – NomadMaker Nov 05 '20 at 20:12
  • If I understand correctly, I am obliged that the total of my percentages is equal to 100%? Let's say I do it, how do I do it from now on? – lucasparmentier388 Nov 05 '20 at 20:14
  • 1
    @lucasparmentier388: well, what happens if you have 2 items with a chance of 100%? Which one will be selected? What is your expected result then? That's why I was pointing out the non-100% total – knittl Nov 05 '20 at 20:15
  • Just get a random integer from 0 - 134. Then loop through the possibilities to find out which one was chosen. – NomadMaker Nov 05 '20 at 20:15
  • 1
    If the sum of probabilities is less than 100%, it must mean that sometimes nothing is returned. Similarly, if the sum of probabilities is greater than 100%, it must mean that sometimes multiple values are returned. In short, unless the sum of probabilities is exactly 100%, the return must be a *list* of values. --- Alternatively, you can reclassify the value as a **weight**, instead of as a *percentage*, in which case they don't have to add up to anything specific, they just all have to be greater than zero. – Andreas Nov 05 '20 at 20:20
  • @knittl In case two items are selected I assume that one of them would be selected randomly, without percentage this time. – lucasparmentier388 Nov 05 '20 at 20:20
  • As an example, if there are 3 values with weights `1, 1, 2`, then it is effectively the same as `25%, 25%, 50%` (sum = 100%). – Andreas Nov 05 '20 at 20:23
  • If the sum is greater than 100%, the second Item cannot be selected randomly without percentage. This would blow the whole point of percentages. If you have three Items 100%, 33.3% and 33.3% one of the 33.3% Items gets selected -> the 100% one must be selected. – ToxicWaste Nov 05 '20 at 20:27
  • I think the easiest way to achieve your task is to generate a random Number between 1 and 100%. Then you wrap all your options around 1 to 100 with a modulo. Visually speaking: You have a wheel of chance and wrap all the possibilities around it. If the sum is < 100 - there is a chance to score nothing. If the sum is > 100 - there is the chance to score multiples. – ToxicWaste Nov 05 '20 at 20:29
  • @ToxicWaste Can you give me an example of how will you do that with the %? – lucasparmentier388 Nov 05 '20 at 20:42

2 Answers2

1

Visually speaking you implement a Wheel of Chance

The wheel will land on any position 1 to 100. Now you just need to read out, which possibilities are at this position. If the sum is < 100 there may be nothing in this position. If the sum is > 100 there may be more than one item in this position.

Whheel of Chacne

All Percentages are wrapping from 1 to 100 - achieved by modulo. The random Number you generate is between 1 and 100, or in this case between 0 and 99 (see Javadoc of Random).

In Code this could look as follows:

public class RandomExample<T extends Object> {
    List<Item> items = new LinkedList<>();
    Random rand = new Random();
    
    public void addItem(int percentage, T object) {
        items.add(new Item(percentage, object));
    }
    
    public List<Item> getRandom() {
        List<Item> result = new ArrayList<>();
        int hit = rand.nextInt(100);
        items.forEach(i -> {
            if(i.isHit(hit)) {
                result.add(i);
            }
        });
        return result;
    }
    
    public class Item {
        private int percentage;
        private T object;
        private int hitLow;
        private int hitHigh;
        
        public Item(int percentage, T object) {
            this.percentage = percentage;
            this.object = object;
            int sum = 0;
            for(Item i : items) {
                sum+=i.getPercentage();
            }
            hitLow = sum % 100;
            hitHigh = (sum+percentage) % 100;
        }
        
        public int getPercentage() {
            return percentage;
        }
        
        public T getObject() {
            return object;
        }
        
        public boolean isHit(int hitVal) {
            boolean isHit = hitLow == hitHigh; //exactly 100%
            isHit |= hitLow <= hitVal && hitHigh >= hitVal;
            return isHit;
        }
    }

}

The method RandomExample.Item.isHit(int) still needs an extra condition to handle chances correctly, which wrap around 100 (eg. hitLow = 90 and hitHigh = 10). But you get the concept and should be able to adapt it to your needs.

Another question you need to ask yourself is: what happens if somebody passes a percentage greater than 100. My intuition says you should throw an exception. But that is for you to decide and implement.

ToxicWaste
  • 135
  • 1
  • 7
  • This would mean that you can only get red and yellow together, but never green and red or green and yellow? Wouldn't you have to check each "color" (enum value) if it would have been picked? I agree that the exact behavior is not clear from the question. – knittl Nov 06 '20 at 06:16
  • True, if there are more than 100% in total only certain combinations are possible. If you want all possible combinations, the wheel does not work. Then I think you need to generate a random number for every Item to determine whether it gets drawn. After that some arithmetic determination is needed to check whether the constraints by the percentages are met - two items wit 90% may still result in no draw, but since the total is 180% at least one needs to be drawn. (at least that is how I understand the question) – ToxicWaste Nov 06 '20 at 08:10
  • Hello, sorry for the past few days, I've been busy. I'm thinking about not using an enumeration anymore, since I'm going to create objects based on a configuration file. I don't really understand the isHit function. If I assume that the sum will be 100, does it make things easier? – lucasparmentier388 Nov 10 '20 at 11:03
  • Hi, the variables ```hitLow``` and ```hitHigh``` only mark the upper and lower boundaries of the wheel, where this option will be selected. The parameter ```hitVal``` in the method ```isHit``` is the value, where the wheel actually stopped. So the method checks whether the selected value is between those boundaries aka. whether the item was selected while spinning the wheel. Assuming each item has a percentage <= 100, you can add this line: ```isHit |= hitLow > hitHigh && hitLow >= hitVal && hitHigh <= hitVal;```. If you have items with percentage > 100%, this behavior needs to change. – ToxicWaste Nov 11 '20 at 10:50
  • For the sake of reusability I would still advise to have some kind of wrapper around the Objects to select from. So your code can be used for Enums, Strings or whatever Object you decide to generate from config-files. To your other question: If you make sure that the sum of all percentages is always 100 - the method of @oleg.cherednik is easier and more performant. To make sure to not have some unexpected behaviors, you should always check that the sum is 100. Especially while using config files (which can be misconfigured), otherwise it may lead to problems nasty to track down. – ToxicWaste Nov 11 '20 at 11:00
1

BE SIMPLE!!!

  1. Create an array.
  2. Put into this array all items, percentage count each.
  3. Shuffle the array.
  4. Retrieve random element form this array.

public class Foo {

    public static void main(String... args) {
        Item[] items = generateItemArray();
        Random random = new Random();

        while (true) {
            System.out.println(items[random.nextInt(items.length)]);
        }
    }

    private static Item[] generateItemArray() {
        List<Item> items = new ArrayList<>();

        for (Item item : Item.values())
            for (int i = 0; i < item.percentage; i++)
                items.add(item);

        Collections.shuffle(items);
        return items.toArray(Item[]::new);
    }

    public enum Item {
        ITEM_1("Item 1", 30),
        ITEM_2("Item 2", 30),
        ITEM_3("Item 3", 10),
        ITEM_4("Item 4", 65);

        private final String name;
        private final int percentage;

        Item(String name, int percentage) {
            this.name = name;
            this.percentage = percentage;
        }

    }
}
Oleg Cherednik
  • 17,377
  • 4
  • 21
  • 35
  • Obviously simpler than my wheel implementation. But handles percentages whose sum are not ```% 100 == 0``` in a way I would assume wrong: If the sum is < 100 there is no chance of returning no value. If the sum > 100 there is no chance to return multiple values. To tackle this issue i suggest filling ```items``` with ```null``` values until ```items.size() % 100 == 0``` and returning ```items.length / 100``` items per draw. Otherwise the percentages are actually weights. – ToxicWaste Nov 05 '20 at 22:56
  • Actually thinking about it: The above changes still do not solve the 100% problem. It is possible to have an Item which should have 100% chance to be picked and still will never be returned. – ToxicWaste Nov 05 '20 at 23:25