001package squidpony.squidmath;
002
003import regexodus.Matcher;
004import regexodus.Pattern;
005import squidpony.annotation.Beta;
006
007import java.io.Serializable;
008import java.util.ArrayList;
009import java.util.Collections;
010import java.util.List;
011
012/**
013 * Class for emulating various traditional RPG-style dice rolls.
014 *
015 * Based on code from the Blacken library.
016 *
017 * @author yam655
018 * @author Eben Howard - http://squidpony.com - howard@squidpony.com
019 */
020@Beta
021public class Dice implements Serializable {
022
023    private static final long serialVersionUID = -488902743486431146L;
024
025    private static final Pattern guessPattern = Pattern.compile("\\s*(\\d+)?\\s*(?:([:])\\s*(\\d+))??\\s*(?:([d:])\\s*(\\d+))?\\s*(?:([+-/*])\\s*(\\d+))?\\s*");
026    private RNG rng;
027
028    /**
029     * Creates a new dice roller that uses a random RNG seed for an RNG that it owns.
030     */
031    public Dice() {
032        rng = new RNG();
033    }
034
035    /**
036     * Creates a new dice roller that uses the given RNG, which can be seeded before it's given here. The RNG will be
037     * shared, not copied, so requesting a random number from the same RNG in another place may change the value of the
038     * next die roll this makes, and dice rolls this makes will change the state of the shared RNG.
039     * @param rng an RNG that can be seeded; will be shared (dice rolls will change the RNG state outside here)
040     */
041    public Dice(RNG rng)
042    {
043        this.rng = rng;
044    }
045
046    /**
047     * Creates a new dice roller that will use its own RNG, seeded with the given seed.
048     * @param seed a long to use as a seed for a new RNG (can also be an int, short, or byte)
049     */
050    public Dice(long seed)
051    {
052        rng = new RNG(seed);
053    }
054    /**
055     * Creates a new dice roller that will use its own RNG, seeded with the given seed.
056     * @param seed a String to use as a seed for a new RNG
057     */
058    public Dice(String seed)
059    {
060        rng = new RNG(seed);
061    }
062    /**
063     * Sets the random number generator to be used.
064     *
065     * This method does not need to be called before using the methods of this
066     * class.
067     *
068     * @param rng the source of randomness
069     */
070    public void setRandom(RNG rng) {
071        this.rng = rng;
072    }
073
074    /**
075     * Rolls the given number of dice with the given number of sides and returns
076     * the total of the best n dice.
077     *
078     * @param n number of best dice to total
079     * @param dice total number of dice to roll
080     * @param sides number of sides on the dice
081     * @return sum of best n out of <em>dice</em><b>d</b><em>sides</em>
082     */
083    public int bestOf(int n, int dice, int sides) {
084        int rolls = Math.min(n, dice);
085        ArrayList<Integer> results = new ArrayList<>(dice);
086
087        for (int i = 0; i < dice; i++) {
088            results.add(rollDice(1, sides));
089        }
090
091        return bestOf(rolls, results);
092    }
093
094    /**
095     * Totals the highest n numbers in the pool.
096     *
097     * @param n the number of dice to be totaled
098     * @param pool the dice to pick from
099     * @return the sum
100     */
101    public int bestOf(int n, List<Integer> pool) {
102        int rolls = Math.min(n, pool.size());
103        Collections.sort(pool);
104
105        int ret = 0;
106        for (int i = 0; i < rolls; i++) {
107            ret += pool.get(pool.size() - 1 - i);
108        }
109        return ret;
110    }
111
112    /**
113     * Find the best n totals from the provided number of dice rolled according
114     * to the roll group string.
115     *
116     * @param n number of roll groups to total
117     * @param dice number of roll groups to roll
118     * @param group string encoded roll grouping
119     * @return the sum
120     */
121    public int bestOf(int n, int dice, String group) {
122        int rolls = Math.min(n, dice);
123        ArrayList<Integer> results = new ArrayList<>(dice);
124
125        for (int i = 0; i < rolls; i++) {
126            results.add(rollGroup(group));
127        }
128
129        return bestOf(rolls, results);
130    }
131
132    /**
133     * Emulate a dice roll and return the sum.
134     *
135     * @param n number of dice to sum
136     * @param sides number of sides on the rollDice
137     * @return sum of rollDice
138     */
139    public int rollDice(int n, int sides) {
140        int ret = 0;
141        for (int i = 0; i < n; i++) {
142            ret += rng.nextInt(sides) + 1;
143        }
144        return ret;
145    }
146
147    /**
148     * Get a list of the independent results of n rolls of dice with the given
149     * number of sides.
150     *
151     * @param n number of dice used
152     * @param sides number of sides on each die
153     * @return list of results
154     */
155    public List<Integer> independentRolls(int n, int sides) {
156        List<Integer> ret = new ArrayList<>(n);
157        for (int i = 0; i < n; i++) {
158            ret.add(rng.nextInt(sides) + 1);
159        }
160        return ret;
161    }
162
163    /**
164     * Turn the string to a randomized number.
165     *
166     * The following types of strings are supported "42": simple absolute string
167     * "10:20": simple random range (inclusive between 10 and 20) "d6": synonym
168     * for "1d6" "3d6": sum of 3 6-sided dice "3:4d6": best 3 of 4 6-sided dice
169     *
170     * The following types of suffixes are supported "+4": add 4 to the value
171     * "-3": subtract 3 from the value "*100": multiply value by 100 "/8":
172     * divide value by 8
173     *
174     * @param group string encoded roll grouping
175     * @return random number
176     */
177    public int rollGroup(String group) {//TODO -- rework to tokenize and allow multiple chained operations
178        Matcher mat = guessPattern.matcher(group);
179        int ret = 0;
180
181        if (mat.matches()) {
182            String num1 = mat.group(1); // number constant
183            String wmode = mat.group(2); // between notation
184            String wnum = mat.group(3); // number constant
185            String mode = mat.group(4); // db
186            String num2 = mat.group(5); // number constant
187            String pmode = mat.group(6); // math operation
188            String pnum = mat.group(7); // number constant
189
190            int a = num1 == null ? 0 : Integer.parseInt(num1);
191            int b = num2 == null ? 0 : Integer.parseInt(num2);
192            int w = wnum == null ? 0 : Integer.parseInt(wnum);
193
194            if (num1 != null && num2 != null) {
195                if (wnum != null) {
196                    if (":".equals(wmode)) {
197                        if ("d".equals(mode)) {
198                            ret = bestOf(a, w, b);
199                        }
200                    }
201                } else if ("d".equals(mode)) {
202                    ret = rollDice(a, b);
203                } else if (":".equals(mode)) {
204                    ret = rng.between(a, b + 1);
205                }
206            } else if (num1 != null) {
207                if (":".equals(wmode)) {
208                    ret = rng.between(a, w + 1);
209                } else {
210                    ret = a;
211                }
212            } else if (num2 != null) {
213                if (mode != null) {
214                    switch (mode) {
215                        case "d":
216                            ret = rollDice(1, b);
217                            break;
218                        case ":":
219                            ret = rng.between(0, b + 1);
220                            break;
221                    }
222                }
223            }
224            if (pmode != null) {
225                int p = pnum == null ? 0 : Integer.parseInt(pnum);
226                switch (pmode) {
227                    case "+":
228                        ret += p;
229                        break;
230                    case "-":
231                        ret -= p;
232                        break;
233                    case "*":
234                        ret *= p;
235                        break;
236                    case "/":
237                        ret /= p;
238                        break;
239                }
240            }
241        }
242        return ret;
243    }
244}