001package squidpony.squidmath;
002
003import regexodus.Matcher;
004import regexodus.Pattern;
005import squidpony.StringKit;
006
007import java.io.Serializable;
008
009/**
010 * Class for emulating various traditional RPG-style dice rolls.
011 * Supports rolling multiple virtual dice of arbitrary size, summing all, the highest <i>n</i>, or the lowest <i>n</i>
012 * dice, treating dice as "exploding" as in some tabletop games (where the max result is rolled again and added),
013 * getting value from inside a range, and applying simple arithmetic modifiers to the result (like adding a number).
014 * Typically you'll want to use the {@link #roll(String)} method if you have a String like {@code "2d8+6"}, or the
015 * various other methods if you have int variables for things like "number of dice to roll" and "sides on each die."
016 * <br>
017 * Based on code from the Blacken library.
018 *
019 * @author yam655
020 * @author Eben Howard - http://squidpony.com - howard@squidpony.com
021 * @author Tommy Ettinger
022 */
023public class Dice implements Serializable {
024
025    private static final long serialVersionUID = -488902743486431146L;
026
027    // The Creature.
028    private static final Matcher mat = Pattern.compile("\\s*(?:(?:(-?\\d+)?\\s*(?:([:><])\\s*(\\d+))?\\s*(?:([d:!])\\s*(\\d+))?)|([+/*-]))\\s*").matcher();
029    private IRNG rng;
030    private transient IntVLA temp = new IntVLA(20);
031    /**
032     * Creates a new dice roller that uses a random RNG seed for an RNG that it owns.
033     */
034    public Dice() {
035        rng = new GWTRNG();
036    }
037
038    /**
039     * Creates a new dice roller that uses the given IRNG, which can be seeded before it's given here. The IRNG will be
040     * shared, not copied, so requesting a random number from the same IRNG in another place may change the value of the
041     * next die roll this makes, and dice rolls this makes will change the state of the shared IRNG.
042     * @param rng an IRNG, such as {@link RNG} or {@link GWTRNG}; will be shared (dice rolls will change the IRNG state outside here)
043     */
044    public Dice(IRNG rng)
045    {
046        this.rng = rng;
047    }
048
049    /**
050     * Creates a new dice roller that will use its own RNG, seeded with the given seed.
051     * @param seed a long to use as a seed for a new RNG (can also be an int, short, or byte)
052     */
053    public Dice(long seed)
054    {
055        rng = new GWTRNG(seed);
056    }
057    /**
058     * Creates a new dice roller that will use its own RNG, seeded with the given seed.
059     * @param seed a String to use as a seed for a new RNG
060     */
061    public Dice(String seed)
062    {
063        rng = new GWTRNG(seed);
064    }
065    /**
066     * Sets the random number generator to be used.
067     *
068     * This method does not need to be called before using the methods of this
069     * class.
070     *
071     * @param rng the source of randomness
072     */
073    public void setRandom(IRNG rng) {
074        this.rng = rng;
075    }
076
077    /**
078     * Rolls the given number of dice with the given number of sides and returns
079     * the total of the best n dice.
080     *
081     * @param n number of best dice to total
082     * @param dice total number of dice to roll
083     * @param sides number of sides on the dice
084     * @return sum of best n out of <em>dice</em><b>d</b><em>sides</em>
085     */
086    private int bestOf(int n, int dice, int sides) {
087        int rolls = Math.min(n, dice);
088        temp.clear();
089        for (int i = 0; i < dice; i++) {
090            temp.add(rollDice(1, sides));
091        }
092
093        return bestOf(rolls, temp);
094
095    }
096
097    /**
098     * Rolls the given number of exploding dice with the given number of sides and returns
099     * the total of the best n dice (counting a die that explodes as one die).
100     *
101     * @param n number of best dice to total
102     * @param dice total number of dice to roll
103     * @param sides number of sides on the dice
104     * @return sum of best n out of <em>dice</em><b>d</b><em>sides</em>
105     */
106    private int bestOfExploding(int n, int dice, int sides) {
107        int rolls = Math.min(n, dice);
108        temp.clear();
109        for (int i = 0; i < dice; i++) {
110            temp.add(rollExplodingDice(1, sides));
111        }
112        return bestOf(rolls, temp);
113    }
114
115    /**
116     * Rolls the given number of dice with the given number of sides and returns
117     * the total of the lowest n dice.
118     *
119     * @param n number of worst dice to total
120     * @param dice total number of dice to roll
121     * @param sides number of sides on the dice
122     * @return sum of worst n out of <em>dice</em><b>d</b><em>sides</em>
123     */
124    private int worstOf(int n, int dice, int sides) {
125        int rolls = Math.min(n, dice);
126        temp.clear();
127        for (int i = 0; i < dice; i++) {
128            temp.add(rollDice(1, sides));
129        }
130        return worstOf(rolls, temp);
131    }
132
133    /**
134     * Rolls the given number of exploding dice with the given number of sides and returns
135     * the total of the lowest n dice (counting a die that explodes as one die).
136     *
137     * @param n number of worst dice to total
138     * @param dice total number of dice to roll
139     * @param sides number of sides on the dice
140     * @return sum of worst n out of <em>dice</em><b>d</b><em>sides</em>
141     */
142    private int worstOfExploding(int n, int dice, int sides) {
143        int rolls = Math.min(n, dice);
144        temp.clear();
145        for (int i = 0; i < dice; i++) {
146            temp.add(rollExplodingDice(1, sides));
147        }
148        return worstOf(rolls, temp);
149    }
150
151    /**
152     * Totals the highest n numbers in the pool.
153     *
154     * @param n the number of dice to be totaled
155     * @param pool the dice to pick from
156     * @return the sum
157     */
158    private int bestOf(int n, IntVLA pool) {
159        int rolls = Math.min(n, pool.size);
160        pool.sort();
161
162        int ret = 0;
163        for (int i = pool.size - 1, r = 0;  r < rolls && i >= 0; i--, r++) {
164            ret += pool.get(i);
165        }
166        return ret;
167    }
168
169    /**
170     * Totals the lowest n numbers in the pool.
171     *
172     * @param n the number of dice to be totaled
173     * @param pool the dice to pick from
174     * @return the sum
175     */
176    private int worstOf(int n, IntVLA pool) {
177        int rolls = Math.min(n, pool.size);
178        pool.sort();
179
180        int ret = 0;
181        for (int r = 0;  r < rolls; r++) {
182            ret += pool.get(r);
183        }
184        return ret;
185    }
186
187    /**
188     * Find the best n totals from the provided number of dice rolled according
189     * to the roll group string.
190     *
191     * @param n number of roll groups to total
192     * @param dice number of roll groups to roll
193     * @param group string encoded roll grouping
194     * @return the sum
195     */
196    public int bestOf(int n, int dice, String group) {
197        int rolls = Math.min(n, dice);
198        temp.clear();
199
200        for (int i = 0; i < dice; i++) {
201            temp.add(roll(group));
202        }
203
204        return bestOf(rolls, temp);
205    }
206
207    /**
208     * Find the worst n totals from the provided number of dice rolled according
209     * to the roll group string.
210     *
211     * @param n number of roll groups to total
212     * @param dice number of roll groups to roll
213     * @param group string encoded roll grouping
214     * @return the sum
215     */
216    public int worstOf(int n, int dice, String group) {
217        int rolls = Math.min(n, dice);
218        temp.clear();
219
220        for (int i = 0; i < dice; i++) {
221            temp.add(roll(group));
222        }
223
224        return worstOf(rolls, temp);
225    }
226
227    /**
228     * Emulate a dice roll and return the sum.
229     *
230     * @param n number of dice to sum
231     * @param sides positive integer; number of sides on the rolled dice
232     * @return sum of rolled dice
233     */
234    public int rollDice(int n, int sides) {
235        int ret = 0;
236        for (int i = 0; i < n; i++) {
237            ret += rng.nextInt(sides) + 1;
238        }
239        return ret;
240    }
241
242    /**
243     * Emulate an exploding dice roll and return the sum.
244     *
245     * @param n number of dice to sum
246     * @param sides number of sides on the rollDice; should be greater than 1
247     * @return sum of rollDice
248     */
249    public int rollExplodingDice(int n, int sides) {
250        int ret = 0, curr;
251        if(sides <= 1) return n; // avoid infinite loop, act like they can't explode
252        for (int i = 0; i < n;) {
253            ret += (curr = rng.nextInt(sides) + 1);
254            if(curr != sides) i++;
255        }
256        return ret;
257    }
258
259    /**
260     * Get a list of the independent results of n rolls of dice with the given
261     * number of sides.
262     *
263     * @param n number of dice used
264     * @param sides positive integer; number of sides on each die
265     * @return list of results
266     */
267    public IntVLA independentRolls(int n, int sides) {
268        IntVLA ret = new IntVLA(n);
269        for (int i = 0; i < n; i++) {
270            ret.add(rng.nextInt(sides) + 1);
271        }
272        return ret;
273    }
274
275    /**
276     * Evaluate the String {@code rollCode} as dice roll notation and roll to get a random result of that dice roll.
277     * This is the main way of using the Dice class. This effectively allows rolling one or more dice and performing
278     * certain operations on the dice and their result. One of the more frequent uses is rolling some amount of dice and
279     * summing their values, which can be done with e.g. "4d10" to roll four ten-sided dice and add up their results.
280     * You can choose to sum only some of the dice, either the "n highest" or "n lowest" values in a group, with "3&gt;4d6"
281     * to sum the three greatest-value dice in four rolls of six-sided dice, or "2&lt;3d8" to sum the two lowest-value dice
282     * in three rolls of eight-sided dice. You can apply modifiers to these results, such as "1d20+7" to roll one
283     * twenty-sided die and add 7 to its result. These modifiers can be other dice, such as "1d10-1d6", and while
284     * multiplication and division are supported, order of operations isn't, so it just rolls dice from left to right
285     * and applies operators it find along the way. You can get a random value in an inclusive range with "50:100",
286     * which is equivalent to "1d51+49" but is easier to read and understand. You can treat dice as "exploding,"
287     * where any dice that get the maximum result are rolled again and added to the total along with the previous
288     * maximum result. As an example, if two exploding six-sided dice are rolled, and their results are 3 and 6, then
289     * because 6 is the maximum value it is rolled again and added to the earlier rolls; if the additional roll is a 5,
290     * then the sum is 3 + 6 + 5 (for a total of 14), but if the additional roll was a 6, then it would be rolled again
291     * and added again, potentially many times if 6 is rolled continually. Some players may be familiar with this game
292     * mechanic from various tabletop games, but many potential players might not be, so it should be explained if you
293     * show the kinds of dice being rolled to players. The syntax used for exploding dice replaced the "d" in "3d6" for
294     * normal dice with "!", making "3!6" for exploding dice. Inclusive ranges are not supported with best-of and
295     * worst-of notation, but exploding dice are. If using a range, the upper bound can be random, decided by dice rolls
296     * such as with "1:6d6" (which rolls six 6-sided dice and uses that as the upper bound of the range) or by other
297     * ranges such as with "10:100:200", which gets a random number between 100 and 200, then returns a random number
298     * between 10 and that. While it is technically allowed to end a dice string with an operator, the partial
299     * operator will be ignored. If you start a dice string with an operator, its left-hand-side will always be 0. If
300     * you have two operators in a row, only the last will be used, unless one is '-' and can be treated as part of a
301     * negative number (this allows "1d20 * -3" to work). Whitespace is allowed between most parts of a dice string.
302     * <br>
303     * The following notation is supported:
304     * <ul>
305     *     <li>{@code 42} : simple absolute string; can start with {@code -} to make it negative</li>
306     *     <li>{@code 3d6} : sum of 3 6-sided dice</li>
307     *     <li>{@code d6} : synonym for {@code 1d6}</li>
308     *     <li>{@code 3>4d6} : best 3 of 4 6-sided dice</li>
309     *     <li>{@code 3:4d6} : gets a random value between 3 and a roll of {@code 4d6}; this syntax has changed</li>
310     *     <li>{@code 2<5d6} : worst 2 of 5 6-sided dice</li>
311     *     <li>{@code 10:20} : simple random range (inclusive between 10 and 20)</li>
312     *     <li>{@code :20} : synonym for {@code 0:20}</li>
313     *     <li>{@code 3!6} : sum of 3 "exploding" 6-sided dice; see above for the semantics of "exploding" dice</li>
314     *     <li>{@code !6} : synonym for {@code 1!6}</li>
315     * </ul>
316     * The following types of operators are supported:
317     * <ul>
318     *     <li>{@code +4} : add 4 to the value</li>
319     *     <li>{@code -3} : subtract 3 from the value</li>
320     *     <li>{@code *100} : multiply value by 100</li>
321     *     <li>{@code /8} : divide value by 8</li>
322     * </ul>
323     * @param rollCode dice string using the above notation
324     * @return a random number that is possible with the given dice string
325     */
326    public int roll(String rollCode) {
327        mat.setTarget(rollCode);
328        int ret, prev = 0;
329        char currentMode = '+';
330        while (mat.find()) {
331            ret = 0;
332            String num1 = mat.group(1); // number constant
333            String wmode = mat.group(2); // between notation
334            String wnum = mat.group(3); // number constant
335            String mode = mat.group(4); // dice, range, or explode
336            String num2 = mat.group(5); // number constant
337            String pmode = mat.group(6); // math operation
338
339            if(pmode != null)
340            {
341                currentMode = pmode.charAt(0);
342                continue;
343            }
344            int a = num1 == null ? 0 : StringKit.intFromDec(num1);
345            int b = num2 == null ? 0 : StringKit.intFromDec(num2);
346            int w = wnum == null ? 0 : StringKit.intFromDec(wnum);
347
348            if (num1 != null && num2 != null) {
349                if (wnum != null) {
350                    if (">".equals(wmode)) {
351                        if ("d".equals(mode)) {
352                            ret = bestOf(a, w, b);
353                        }
354                        else if("!".equals(mode))
355                        {
356                            ret = bestOfExploding(a, w, b);
357                        }
358                    }
359                    else if("<".equals(wmode))
360                    {
361                        if ("d".equals(mode)) {
362                            ret = worstOf(a, w, b);
363                        }
364                        else if("!".equals(mode))
365                        {
366                            ret = worstOfExploding(a, w, b);
367                        }
368                    }
369                    else
370                    // Here, wmode is ":", there is a constant lower bound for the range, and the upper bound is some
371                    // dice roll or other range. This can be negative, easily, if the random upper bound is negative
372                    {
373                        if ("d".equals(mode)) {
374                            ret = a + rng.nextSignedInt(rollDice(w, b) + 1 - a);
375                        } else if ("!".equals(mode)) {
376                            ret = a + rng.nextSignedInt(rollExplodingDice(w, b) + 1 - a);
377                        } else if (":".equals(mode)) {
378                            ret = a + rng.nextSignedInt(w + rng.nextSignedInt(b + 1 - w) + 1 - a);
379                        }
380                    }
381                } else if ("d".equals(mode)) {
382                    ret = rollDice(a, b);
383                } else if ("!".equals(mode)) {
384                    ret = rollExplodingDice(a, b);
385                } else if (":".equals(mode)) {
386                    ret = a + rng.nextSignedInt(b + 1 - a);
387                }
388            } else if (num1 != null) {
389                if (":".equals(wmode)) {
390                    ret = a + rng.nextSignedInt(w + 1 - a);
391                } else {
392                    ret = a;
393                }
394            } else if (num2 != null) {
395                if (mode != null) {
396                    switch (mode) {
397                        case "d":
398                            ret = rollDice(1, b);
399                            break;
400                        case "!":
401                            ret = rollExplodingDice(1, b);
402                            break;
403                        case ":":
404                            ret = rng.nextSignedInt(b + 1);
405                            break;
406                    }
407                }
408            }
409            switch (currentMode)
410            {
411                case '-':
412                    prev -= ret;
413                    break;
414                case '*':
415                    prev *= ret;
416                    break;
417                case '/':
418                    prev /= ret;
419                    break;
420                default:
421                    prev += ret;
422                    break;
423            }
424            currentMode = '+';
425        }
426        return prev;
427    }
428}