// // die.cxx // dice-roll // // Created by Sam Jaffe on 12/1/18. // Copyright © 2018 Sam Jaffe. All rights reserved. // #include "dice-roll/die.h" #include #include #include "dice-roll/exception.h" static void advance_over_whitespace(std::istream & in, char const * also = "") { if (strchr(also, in.peek())) { in.get(); } while (isspace(in.peek())) { in.get(); } } namespace dice { int sgn(sign s) { return s == MINUS ? -1 : 1; } std::string str(sign s) { switch (s) { case PLUS: return "+"; case MINUS: return "-"; default: return ""; } } mod::operator int() const { return sgn(sign) * value; } std::ostream & operator<<(std::ostream & out, dice const & d) { if (d.num != 1) out << d.num << '{'; for (die const & di : d.of) { out << str(di.sgn) << di.num << 'd' << di.sides; } for (mod m : d.modifier) { out << str(m.sign) << m.value; } if (d.num != 1) out << '}'; return out; } } namespace dice { namespace { struct parser { void parse(sign s); void parse_dN(sign s, int value); void parse_const(sign s, int value); std::istream & in; dice & d; }; /** * @sideeffect This function advances the input stream over a single numeric * token. This token represents the number of sides in the die roll. * This function appends a new die into {@see d.of}, representing (+/-)NdM, * where N is the input parameter 'value', and M is retrieved internally. * @param s The arithmatic sign attached to this roll, denoting whether this * die increases or decreases the total result of the roll. For example, the * 5E spell Bane imposes a -1d4 modifier on attack rolls, an attack roll might * go from '1d20+2+3' (1d20 + Proficiency + Ability) to '1d20+2+3-1d4'. * @param value The number of dice to be rolled. * Domain: value >= 0 * @throw dice::unexpected_token if we somehow call parse_dN while the first * non-whitespace token after the 'd' char is not a number. */ void parser::parse_dN(sign s, int value) { advance_over_whitespace(in, "dD"); // Disallow 0dM, as that is not a real thing... d.of.push_back({s, std::max(value, 1), 0}); if (!isnumber(in.peek())) { throw unexpected_token("Expected a number of sides", in.tellg()); } in >> d.of.back().sides; parse(ZERO); } /** * @param s The arithmatic sign attached to this numeric constant. Because * value is non-negative, this token contains the +/- effect. * @param value The value associated with this modifier term. * Domain: value >= 0 */ void parser::parse_const(sign s, int value) { if (value) { // Zero is not a modifier we care about d.modifier.push_back({s, std::abs(value)}); } } /** * Main dispatch function for parsing a dice roll. * @param s The current +/- sign attached to the parse sequence. s is ZERO * when parsing the first token, or after parsing a die. This means an * expression like '1d4+5+1d6+2d8' is evaluated as a sequence like so: * [1d4][+][5+][1d6][+][2d8]. This produces the following states of (SIGN, * input stream): * 1) ZERO, 1d4+5+1d6+2d8 * 2) ZERO, +5+1d6+2d8 * 3) PLUS, 5+1d6+2d8 * 4) PLUS, 1d6+2d8 * 5) ZERO, +2d8 * 6) PLUS, 2d8 */ void parser::parse(sign s) { advance_over_whitespace(in); // By defaulting this to zero, we can write a more elegant handling of // expressions like 1d4+1d6+5+1 int value = 0; if (isnumber(in.peek())) { in >> value; } else if (in.peek() == EOF && s != ZERO) { throw unexpected_token("Unexpected EOF while parsing", -1); } advance_over_whitespace(in); switch (in.peek()) { case 'd': case 'D': return parse_dN(s, value); case '+': case '-': // Handle 5+... cases parse_const(s, value); // Add another token parse((in.get() == '+') ? PLUS : MINUS); break; default: parse_const(s, value); break; } } }} namespace dice { std::istream & operator>>(std::istream & in, dice & d) { int value{1}; advance_over_whitespace(in); if (isnumber(in.peek())) { in >> value; } advance_over_whitespace(in); switch (in.peek()) { case 'd': case 'D': parser{in, d}.parse(ZERO); d.of.front().num = value; break; case '{': in.get(); d.num = value; parser{in, d}.parse(ZERO); if (in.get() != '}') { throw unexpected_token("Expected closing '}' in repeated roll", in.tellg()); } break; case EOF: throw unexpected_token("No dice in expression", in.tellg()); default: throw unexpected_token("Unexpected token", in.tellg()); } return in; } dice from_string(std::string const & str) { std::stringstream ss(str); dice d; ss >> d; return d; } }