/* * RollInfo.java * Copyright 2001 (C) Bryan McRoberts <merton_monk@yahoo.com> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Created on April 21, 2001, 2:15 PM * * $Id$ */ package pcgen.core; import pcgen.util.Logging; import java.util.StringTokenizer; import org.apache.commons.lang3.StringUtils; /** * {@code RollInfo}. * * Structure representing dice rolls * * @author binkley */ public final class RollInfo { public int getSides() { return sides; } public int getTimes() { return times; } /** What shape dice to roll. */ protected int sides = 0; /** Number of dice to roll. */ protected int times = 0; /** Which specific rolls to keep after rolls have been sorted * in ascending order. {@code null} means to keep all * rolls. Example, [1,3] means to keep the first and third * lowest rolls, which would be {true, false true} for 3 dice. * keepTop and keepBottom are implemented as special kinds of * this array. */ private boolean[] keepList = null; /** Amount to add to the final roll. */ private int modifier = 0; /** Rerolls rolls above this amount. */ private int rerollAbove = Integer.MAX_VALUE; /** Rerolls rolls below this amount. */ private int rerollBelow = Integer.MIN_VALUE; /** Total result never greater than this. */ private int totalCeiling = Integer.MAX_VALUE; /** Total result never less than this. */ private int totalFloor = Integer.MIN_VALUE; /** * Check that the rollString is valid. * @param rollString The string to be checked * @return An empty string if the string is valid, an error message if not. */ public static String validateRollString(String rollString) { return parseRollInfo(new RollInfo(), rollString); } public static String parseRollInfo(RollInfo rollInfo, String rollString) { // To really do this right, we change the token string // as we go along so that we maintain parser state by // means of the tokens rather than something more // explicit. In truth, this is an ideal application // of flex and friends for a "mini-language" whose // statements evaluate to dice rolls. Too much LISP // on the brain. --bko final StringTokenizer st = new StringTokenizer(rollString, " ", true); try { String tok = st.nextToken("d"); if ("d".equals(tok)) { rollInfo.times = 1; } else { rollInfo.times = Integer.parseInt(tok); if (st.hasMoreTokens()) { tok = st.nextToken("d"); // discard the 'd' if (!"d".equals(tok)) { return "Bad roll parsing in '" + rollString + "': missing 'd'"; } } else { rollInfo.sides = 1; return ""; } } String parseChars = "/\\|mM+-tT"; rollInfo.sides = Integer.parseInt(st.nextToken(parseChars)); if (rollInfo.sides < 1) { return "Bad roll parsing in '" + rollString + "': sides < 1: " + rollInfo.sides; } while (st.hasMoreTokens()) { tok = st.nextToken(parseChars); switch (tok.charAt(0)) { case '/': parseChars = "mM+-tT"; final int keepTop = Integer.parseInt(st.nextToken(parseChars)); if (keepTop > rollInfo.times) { return "Bad keepTop in '" + rollString + "': times: " + rollInfo.times + "; keepTop: " + keepTop; } rollInfo.keepList = new boolean[rollInfo.times]; // Rely on fact boolean is false by default. --bko for (int i = rollInfo.times - keepTop; i < rollInfo.times; ++i) { rollInfo.keepList[i] = true; } break; case '\\': parseChars = "mM+-tT"; final int keepBottom = Integer.parseInt(st.nextToken(parseChars)); if (keepBottom > rollInfo.times) { return "Bad keepBottom in '" + rollString + "': times: " + rollInfo.times + "; keepBottom: " + keepBottom; } rollInfo.keepList = new boolean[rollInfo.times]; // Rely on fact boolean is false by default. --bko for (int i = 0; i < keepBottom; ++i) { rollInfo.keepList[i] = true; } break; case '|': parseChars = "mM+-tT"; tok = st.nextToken(parseChars); rollInfo.keepList = new boolean[rollInfo.times]; final StringTokenizer keepSt = new StringTokenizer(tok, ","); while (keepSt.hasMoreTokens()) { rollInfo.keepList[Integer.parseInt(keepSt .nextToken(",")) - 1] = true; } break; case 'm': parseChars = "M+-tT"; rollInfo.rerollBelow = Integer.parseInt(st.nextToken(parseChars)); break; case 'M': parseChars = "m+-tT"; rollInfo.rerollAbove = Integer.parseInt(st.nextToken(parseChars)); break; case '+': parseChars = "tT"; rollInfo.modifier = Integer.parseInt(st.nextToken(" ")); break; case '-': parseChars = "tT"; rollInfo.modifier = -Integer.parseInt(st.nextToken(" ")); break; case 't': parseChars = "T"; rollInfo.totalFloor = Integer.parseInt(st.nextToken(" ")); break; case 'T': parseChars = "t"; rollInfo.totalCeiling = Integer.parseInt(st.nextToken(" ")); break; default: Logging.errorPrint("Bizarre dice parser error in '" + rollString + "': not a valid delimiter"); return "Bad roll parsing in '" + rollString + "': invalid delimiter '" + tok.charAt(0) + "'."; } } } catch (NumberFormatException ex) { if (Logging.isDebugMode()) { Logging.debugPrint("Bad roll string in '" + rollString + "': " + ex, ex); } return "Bad roll string in '" + rollString + "': " + ex; } return ""; } /** * Private constructor for use only when validating a roll string. */ private RollInfo() { } /** * Construct a {@code RollInfo} from a string. The * rules:<ol> * * <li>Optional positive integer, <var>times</var>.</li> * * <li>Literal 'd' followed by positive integer, * <var>sides</var>.</li> * * <li>Optional literal '/' followed by positive integer, * <var>keepTop</var>, or literal '\' followed by positive * integer, <var>keepBottom</var>, or literal '|' followed by * comma-separated list of postitive integers, * <var>keepList</var> (1-indexed after dice have been * sorted).</li> * * <li>Optional literal 'm' (minimum) followed by positive * integer, <var>rerollAbove</var>, or literal 'M' (maximum) * followed by postive integer, <var>rerollBelow</var>.</li> * * <li>Optional literal '+' or '-' followed by positive * integer, <var>modifier</var>.</li> * * <li>Optional literal 't' followed by positive integer, * <var>totalFloor</var>, or literal 'T' followed by a * positive *integer, <var>totalCeiling</var>.</li> * * </ol> Unlike previous versions of this method, it is * <strong>case-sensitive</strong> with respect to the * alphabetic characters, e.g., only {@code d} * (lower-case) is now valid, not also {@code D} * (upper-case). This is to accommodate the expanded ways to * roll. * * @param rollString String compact representation of dice rolls * */ public RollInfo(final String rollString) { String errMsg = RollInfo.parseRollInfo(this, rollString); if (!StringUtils.isBlank(errMsg)) { Logging.errorPrint(errMsg); } } /** * Main method * Boy, does this need testing! * @param args */ public static void main(final String[] args) { Logging.setDebugMode(true); for (int i = 0; i < args.length; ++i) { final RollInfo ri = new RollInfo(args[i]); Logging.debugPrint(ri + ": " + RollInfo.roll()); } } @Override public String toString() { final StringBuilder buf = new StringBuilder(50); if (times > 0) { buf.append(times); } buf.append("d").append(sides); while (keepList != null) // let break work { int p; int i; for (i = 0; i < times; ++i) { if (keepList[i]) { break; } } if (i == times) // all false { Logging.errorPrint("Bad rolls: nothing to keep!"); return null; } // Note the ordering: by testing for bottom // first, we can also test if all the dice are // all to be kept, and drop the // top/bottom/list specification completely. // First test for bottom for (i = 0; i < times; ++i) { if (!keepList[i]) { break; } } if (i == times) { break; // all true } p = i; for (; i < times; ++i) { if (keepList[i]) { break; } } if ((p > 0) && (i == times)) { buf.append("\\").append(p); break; } // Second test for top for (i = 0; i < times; ++i) { if (keepList[i]) { break; } } p = i; for (; i < times; ++i) { if (!keepList[i]) { break; } } if ((p > 0) && (i == times)) { buf.append("/").append((times - p)); break; } // Finally, we have a list buf.append("|"); boolean first = true; for (i = 0; i < times; ++i) { if (!keepList[i]) { continue; } if (first) { first = false; } else { buf.append(","); } buf.append(i + 1); } } if (rerollBelow != Integer.MIN_VALUE) { buf.append("m").append(rerollBelow); } if (rerollAbove != Integer.MAX_VALUE) { buf.append("M").append(rerollAbove); } if (modifier > 0) { buf.append("+").append(modifier); } else if (modifier < 0) { buf.append("-").append(-modifier); } if (totalFloor != Integer.MIN_VALUE) { buf.append("t").append(totalFloor); } if (totalCeiling != Integer.MAX_VALUE) { buf.append("T").append(totalCeiling); } return buf.toString(); } /** * Roll the dice. UNIMPLEMENTED FOR NOW! * * @return int the results */ private static int roll() { final int result = 0; return result; } }