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>4d6" 281 * to sum the three greatest-value dice in four rolls of six-sided dice, or "2<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}