die.cxx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. //
  2. // die.cxx
  3. // dice-roll
  4. //
  5. // Created by Sam Jaffe on 12/1/18.
  6. // Copyright © 2018 Sam Jaffe. All rights reserved.
  7. //
  8. #include "dice-roll/die.h"
  9. #include <iostream>
  10. #include <sstream>
  11. #include "dice-roll/exception.h"
  12. static void advance_over_whitespace(std::istream & in, char const * also = "") {
  13. if (strchr(also, in.peek())) { in.get(); }
  14. while (isspace(in.peek())) {
  15. in.get();
  16. }
  17. }
  18. namespace dice {
  19. int sgn(sign s) { return s == MINUS ? -1 : 1; }
  20. std::string str(sign s) {
  21. switch (s) {
  22. case PLUS:
  23. return "+";
  24. case MINUS:
  25. return "-";
  26. default:
  27. return "";
  28. }
  29. }
  30. mod::operator int() const { return sgn(sign) * value; }
  31. std::ostream & operator<<(std::ostream & out, dice const & d) {
  32. if (d.num != 1) out << d.num << '{';
  33. for (die const & di : d.of) {
  34. out << str(di.sgn) << di.num << 'd' << di.sides;
  35. }
  36. for (mod m : d.modifier) {
  37. out << str(m.sign) << m.value;
  38. }
  39. if (d.num != 1) out << '}';
  40. return out;
  41. }
  42. }
  43. namespace dice { namespace {
  44. struct parser {
  45. void parse(sign s);
  46. void parse_dN(sign s, int value);
  47. void parse_const(sign s, int value);
  48. std::istream & in;
  49. dice & d;
  50. };
  51. /**
  52. * @sideeffect This function advances the input stream over a single numeric
  53. * token. This token represents the number of sides in the die roll.
  54. * This function appends a new die into {@see d.of}, representing (+/-)NdM,
  55. * where N is the input parameter 'value', and M is retrieved internally.
  56. * @param s The arithmatic sign attached to this roll, denoting whether this
  57. * die increases or decreases the total result of the roll. For example, the
  58. * 5E spell Bane imposes a -1d4 modifier on attack rolls, an attack roll might
  59. * go from '1d20+2+3' (1d20 + Proficiency + Ability) to '1d20+2+3-1d4'.
  60. * @param value The number of dice to be rolled.
  61. * Domain: value >= 0
  62. * @throw dice::unexpected_token if we somehow call parse_dN while the first
  63. * non-whitespace token after the 'd' char is not a number.
  64. */
  65. void parser::parse_dN(sign s, int value) {
  66. advance_over_whitespace(in, "dD");
  67. // Disallow 0dM, as that is not a real thing...
  68. d.of.push_back({s, std::max(value, 1), 0});
  69. if (!isnumber(in.peek())) {
  70. throw unexpected_token("Expected a number of sides", in.tellg());
  71. }
  72. in >> d.of.back().sides;
  73. parse(ZERO);
  74. }
  75. /**
  76. * @param s The arithmatic sign attached to this numeric constant. Because
  77. * value is non-negative, this token contains the +/- effect.
  78. * @param value The value associated with this modifier term.
  79. * Domain: value >= 0
  80. */
  81. void parser::parse_const(sign s, int value) {
  82. if (value) { // Zero is not a modifier we care about
  83. d.modifier.push_back({s, std::abs(value)});
  84. }
  85. }
  86. /**
  87. * Main dispatch function for parsing a dice roll.
  88. * @param s The current +/- sign attached to the parse sequence. s is ZERO
  89. * when parsing the first token, or after parsing a die. This means an
  90. * expression like '1d4+5+1d6+2d8' is evaluated as a sequence like so:
  91. * [1d4][+][5+][1d6][+][2d8]. This produces the following states of (SIGN,
  92. * input stream):
  93. * 1) ZERO, 1d4+5+1d6+2d8
  94. * 2) ZERO, +5+1d6+2d8
  95. * 3) PLUS, 5+1d6+2d8
  96. * 4) PLUS, 1d6+2d8
  97. * 5) ZERO, +2d8
  98. * 6) PLUS, 2d8
  99. */
  100. void parser::parse(sign s) {
  101. advance_over_whitespace(in);
  102. // By defaulting this to zero, we can write a more elegant handling of
  103. // expressions like 1d4+1d6+5+1
  104. int value = 0;
  105. if (isnumber(in.peek())) {
  106. in >> value;
  107. } else if (in.peek() == EOF && s != ZERO) {
  108. throw unexpected_token("Unexpected EOF while parsing", -1);
  109. }
  110. advance_over_whitespace(in);
  111. switch (in.peek()) {
  112. case 'd':
  113. case 'D':
  114. return parse_dN(s, value);
  115. case '+':
  116. case '-':
  117. // Handle 5+... cases
  118. parse_const(s, value);
  119. // Add another token
  120. parse((in.get() == '+') ? PLUS : MINUS);
  121. break;
  122. default:
  123. parse_const(s, value);
  124. break;
  125. }
  126. }
  127. }}
  128. namespace dice {
  129. std::istream & operator>>(std::istream & in, dice & d) {
  130. int value{1};
  131. advance_over_whitespace(in);
  132. if (isnumber(in.peek())) { in >> value; }
  133. advance_over_whitespace(in);
  134. switch (in.peek()) {
  135. case 'd':
  136. case 'D':
  137. parser{in, d}.parse(ZERO);
  138. d.of.front().num = value;
  139. break;
  140. case '{':
  141. in.get();
  142. d.num = value;
  143. parser{in, d}.parse(ZERO);
  144. if (in.get() != '}') {
  145. throw unexpected_token("Expected closing '}' in repeated roll",
  146. in.tellg());
  147. }
  148. break;
  149. case EOF:
  150. throw unexpected_token("No dice in expression", in.tellg());
  151. default:
  152. throw unexpected_token("Unexpected token", in.tellg());
  153. }
  154. return in;
  155. }
  156. dice from_string(std::string const & str) {
  157. std::stringstream ss(str);
  158. dice d;
  159. ss >> d;
  160. return d;
  161. }
  162. }