Przeglądaj źródła

Merge branch 'feat/difficulty-class'

* feat/difficulty-class:
  Add roll support for difficulty_class.
  Add more tests for dice_roll.
  Move roll ostream<< functions into io.cxx
  Move IO tests into separate test file.
  Add support for printing out DC info. Move stringification of sign enum into an ostream<<.
  Reach 100% code coverage.
  Fix naming.
  Add test cases for difficulty_class parsing.
  Some cleanup
  Fixing comments a little.
  Add difficulty_class to dice.
  Add difficulty_class object and unit tests.
  Fix indenting.
  Add schema info for parser.
  Rename dice_test => parser_test
  Make sign an enum-class.
Sam Jaffe 4 lat temu
rodzic
commit
960976f87f

+ 12 - 4
dice-roll.xcodeproj/project.pbxproj

@@ -12,7 +12,7 @@
 		CD38F50F21C83936007A732C /* libdice.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD38F50921C83912007A732C /* libdice.dylib */; };
 		CD38F51221C8397A007A732C /* libshared_random_generator.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CDED6A6021B2F89900AB91D0 /* libshared_random_generator.dylib */; };
 		CD38F52B21C87771007A732C /* libdice.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD38F50921C83912007A732C /* libdice.dylib */; };
-		CD38F53521C87799007A732C /* dice_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD38F53421C87799007A732C /* dice_test.cxx */; };
+		CD38F53521C87799007A732C /* parser_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD38F53421C87799007A732C /* parser_test.cxx */; };
 		CD38F53721C89493007A732C /* exception.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD38F53621C89493007A732C /* exception.cxx */; };
 		CD38F53921C922E2007A732C /* roll_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD38F53821C922E2007A732C /* roll_test.cxx */; };
 		CD38F53B21C928B4007A732C /* exception_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD38F53A21C928B4007A732C /* exception_test.cxx */; };
@@ -24,6 +24,8 @@
 		CDED6A2721B2F28A00AB91D0 /* main.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDED6A2621B2F28A00AB91D0 /* main.cxx */; };
 		CDEE78F725B336B000F195F9 /* parser.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDEE78F625B336B000F195F9 /* parser.cxx */; };
 		CDEE790225B336EC00F195F9 /* io.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDEE790125B336EC00F195F9 /* io.cxx */; };
+		CDEE7A6B25B34DAA00F195F9 /* die_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDEE7A6A25B34DAA00F195F9 /* die_test.cxx */; };
+		CDEE7A7525B35EEC00F195F9 /* io_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDEE7A7425B35EEC00F195F9 /* io_test.cxx */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -103,7 +105,7 @@
 		CD38F50921C83912007A732C /* libdice.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libdice.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
 		CD38F52621C87771007A732C /* dice-td.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "dice-td.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
 		CD38F52A21C87771007A732C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
-		CD38F53421C87799007A732C /* dice_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = dice_test.cxx; sourceTree = "<group>"; };
+		CD38F53421C87799007A732C /* parser_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = parser_test.cxx; sourceTree = "<group>"; };
 		CD38F53621C89493007A732C /* exception.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = exception.cxx; sourceTree = "<group>"; };
 		CD38F53821C922E2007A732C /* roll_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = roll_test.cxx; sourceTree = "<group>"; };
 		CD38F53A21C928B4007A732C /* exception_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = exception_test.cxx; sourceTree = "<group>"; };
@@ -121,6 +123,8 @@
 		CDEE78F625B336B000F195F9 /* parser.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = parser.cxx; sourceTree = "<group>"; };
 		CDEE790125B336EC00F195F9 /* io.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = io.cxx; sourceTree = "<group>"; };
 		CDEE7A5925B3437D00F195F9 /* terminal_helper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = terminal_helper.h; sourceTree = "<group>"; };
+		CDEE7A6A25B34DAA00F195F9 /* die_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = die_test.cxx; sourceTree = "<group>"; };
+		CDEE7A7425B35EEC00F195F9 /* io_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = io_test.cxx; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -238,8 +242,10 @@
 		CDED6A2D21B2F2B200AB91D0 /* test */ = {
 			isa = PBXGroup;
 			children = (
-				CD38F53421C87799007A732C /* dice_test.cxx */,
+				CD38F53421C87799007A732C /* parser_test.cxx */,
+				CDEE7A6A25B34DAA00F195F9 /* die_test.cxx */,
 				CD38F53821C922E2007A732C /* roll_test.cxx */,
+				CDEE7A7425B35EEC00F195F9 /* io_test.cxx */,
 				CD38F53A21C928B4007A732C /* exception_test.cxx */,
 				CDEE78ED25B3350B00F195F9 /* xcode_gtest_helper.h */,
 			);
@@ -448,7 +454,9 @@
 			files = (
 				CD38F53B21C928B4007A732C /* exception_test.cxx in Sources */,
 				CD38F53921C922E2007A732C /* roll_test.cxx in Sources */,
-				CD38F53521C87799007A732C /* dice_test.cxx in Sources */,
+				CDEE7A6B25B34DAA00F195F9 /* die_test.cxx in Sources */,
+				CDEE7A7525B35EEC00F195F9 /* io_test.cxx in Sources */,
+				CD38F53521C87799007A732C /* parser_test.cxx in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 58 - 38
include/dice-roll/die.h

@@ -9,46 +9,66 @@
 #pragma once
 
 #include <iosfwd>
+#include <string>
 #include <vector>
 
 namespace dice {
-  enum sign { PLUS = 1, MINUS = -1, ZERO = 0 };
-  template <typename T> static sign sgn(T val) {
-    return sign((T(0) < val) - (val < T(0)));
-  }
-  int sgn(sign);
-  std::string str(sign);
-
-  struct die {
-    sign sgn;
-    int num, sides;
-  };
-
-  struct mod {
-    operator int() const;
-    sign sign;
-    int value;
-  };
-
-  // Default value: 1{+0}
-  struct dice {
-    int num{1};
-    std::vector<die> of{};
-    std::vector<mod> modifier{+0};
-  };
+enum class sign { PLUS = 1, MINUS = -1, ZERO = 0 };
+
+template <typename T> static sign sgn(T val) {
+  return sign((T(0) < val) - (val < T(0)));
+}
+
+int sgn(sign);
+
+struct die {
+  sign sgn;
+  int num, sides;
+};
+
+struct mod {
+  operator int() const;
+  sign sign;
+  int value;
+};
+
+/**
+ * In some cases, the roller is not interested in the actual value of the dice
+ * rolled, but only if they pass some metric.
+ * This object allows us to perform that check, and then conveniently obfuscate
+ * the numbers rolled so that the player cannot intuit their odds of success in
+ * the short term.
+ */
+struct difficulty_class {
+  enum class test { None, Less, LessOrEqual, Greater, GreaterOrEqual };
+  
+  bool operator()(int value) const;
   
-  /**
-   * @brief A generator function to turn a string representation of a dice roll into a C++ object
-   * @param strdice A string representation of a dice roll, represented as one of the
-   * following expression classes:
-   * Die = [1-9]?\d*d[1-9]\d*
-   * SingleRoll: ($Die|\d+)((+|-)($Die|\d+))*
-   * RepeatRoll: [1-9]\d*\{$SingleRoll\}
-   * @return a dice object representing the roll
-   * @throws dice::unexpected_token if a parse failure occurs
-   */
-  dice from_string(std::string const & strdice);
-
-  std::ostream & operator<<(std::ostream & out, dice const & d);
-  std::istream & operator>>(std::istream & out, dice & d);
+  test comp{test::None};
+  int against{0};
+};
+
+// Default value: 1{+0}
+struct dice {
+  int num{1};
+  std::vector<die> of{};
+  std::vector<mod> modifier{+0};
+  difficulty_class dc{};
+};
+
+/**
+ * @brief A generator function to turn a string representation of a dice roll into a C++ object
+ * @param strdice A string representation of a dice roll, represented as one of the
+ * following expression classes:
+ * Die = [1-9]?\d*d[1-9]\d*
+ * SingleRoll: ($Die|\d+)((+|-)($Die|\d+))*
+ * RepeatRoll: [1-9]\d*\{$SingleRoll\}
+ * @return a dice object representing the roll
+ * @throws dice::unexpected_token if a parse failure occurs
+ */
+dice from_string(std::string const & strdice);
+
+std::ostream & operator<<(std::ostream & out, sign s);
+std::ostream & operator<<(std::ostream & out, dice const & d);
+std::istream & operator>>(std::istream & out, dice & d);
 }

+ 22 - 22
include/dice-roll/exception.h

@@ -11,28 +11,28 @@
 #include <stdexcept>
 
 namespace dice {
-  class unexpected_token : public std::runtime_error {
-  public:
-    /**
-     * @brief An error for failure to parse a string into a dice::dice object
-     * @param reason The specific reason that this error occured
-     * @param position The index into the string that the error was located at,
-     * or -1(EOF) if the error was a 'we ran out of buffer' type.
-     */
-    unexpected_token(std::string const & reason, long long position);
+class unexpected_token : public std::runtime_error {
+public:
+  /**
+   * @brief An error for failure to parse a string into a dice::dice object
+   * @param reason The specific reason that this error occured
+   * @param position The index into the string that the error was located at,
+   * or -1(EOF) if the error was a 'we ran out of buffer' type.
+   */
+  unexpected_token(std::string const & reason, long long position);
 
-    /**
-     * @brief Create a string matching the regex '~*^', that points to the error
-     * covered by this exception class.
-     * @param backup_length If {@see unexpected_token::position} == -1, use
-     * this value instead as N.
-     * @return A string of N '~' and one '^', pointing to the character in the
-     * parse string that the error occured at.
-     * "<END>" if position and backup_length are both -1
-     */
-    std::string pointer(long long backup_length = -1) const;
+  /**
+   * @brief Create a string matching the regex '~*^', that points to the error
+   * covered by this exception class.
+   * @param backup_length If {@see unexpected_token::position} == -1, use
+   * this value instead as N.
+   * @return A string of N '~' and one '^', pointing to the character in the
+   * parse string that the error occured at.
+   * "<END>" if position and backup_length are both -1
+   */
+  std::string pointer(long long backup_length = -1) const;
 
-  private:
-    long long position;
-  };
+private:
+  long long position;
+};
 }

+ 24 - 0
include/dice-roll/parser.h

@@ -14,6 +14,29 @@
 
 namespace dice {
 
+/*
+ * A parser that translates a stream of text input into a dice object. The
+ * string matches the following schema-rule:
+ *
+ * dice-string:
+ *   dice-expression
+ *   positive-integer '{' dice-expression '}'
+ *
+ * dice-expression:
+ *   die-expression
+ *   die-expression {'+'|'-'} dice-expression-rec
+ *
+ * dice-expression-rec:
+ *   {die-expression|modifier}
+ *   {die-expression|modifier} {'+'|'-'} dice-expression-rec
+ *
+ * modifier:
+ *   positive-integer
+ *
+ * die-expression:
+ *   positive-integer 'd' positive-integer
+ *   positive-integer 'D' positive-integer
+ */
 class parser {
 private:
   std::istream & is_;
@@ -62,6 +85,7 @@ private:
    * Domain: value >= 0
    */
   void parse_const(sign s, int value);
+  void parse_dc(char token);
 };
 
 }

+ 15 - 15
include/dice-roll/random.h

@@ -11,20 +11,20 @@
 #include <shared_random_generator/random.h>
 
 namespace dice { namespace engine {
-  class random : private ::engine::random_number_generator {
-    using super = ::engine::random_number_generator;
+class random : private ::engine::random_number_generator {
+  using super = ::engine::random_number_generator;
 
-  public:
-    using super::random_number_generator;
-    /**
-     * @brief Roll 1dN
-     * @param sides The number of sides (N) on a die. e.g. d20
-     * Domain: sides > 0
-     * @return A number in the range [1, N]
-     * @throws Produces UB if sides = 0
-     */
-    unsigned int roll(unsigned int sides) const {
-      return super::exclusive(sides) + 1;
-    }
-  };
+public:
+  using super::random_number_generator;
+  /**
+   * @brief Roll 1dN
+   * @param sides The number of sides (N) on a die. e.g. d20
+   * Domain: sides > 0
+   * @return A number in the range [1, N]
+   * @throws Produces UB if sides = 0
+   */
+  unsigned int roll(unsigned int sides) const {
+    return super::exclusive(sides) + 1;
+  }
+};
 }}

+ 40 - 39
include/dice-roll/roll.h

@@ -14,47 +14,48 @@
 #include "random.h"
 
 namespace dice {
-  // Describe the actual result of rolling (+/-)NdM
-  struct die_roll {
-    // Collapse this roll into its actual value
-    operator int() const;
-    // Is this being added, or subtracted from the total
-    sign sign;
-    // Since this roll was composed on NdM, rolled.size() == N. Each element
-    // of rolled is within the integer range [1, M].
-    std::vector<int> rolled;
-  };
+// Describe the actual result of rolling (+/-)NdM
+struct die_roll {
+  // Collapse this roll into its actual value
+  operator int() const;
+  // Is this being added, or subtracted from the total
+  sign sign;
+  // Since this roll was composed on NdM, rolled.size() == N. Each element
+  // of rolled is within the integer range [1, M].
+  std::vector<int> rolled;
+};
 
-  // Describe the actual result of rolling an arbitrary set of dice with mods
-  struct dice_roll {
-    // Collapse this roll into its actual value
-    operator int() const;
-    // A vector of component roll results, each on representing a single NdM
-    // expression.
-    std::vector<die_roll> sub_rolls;
-    // A vector of every modifier attached to the system.
-    std::vector<mod> modifiers;
-  };
-  
-  class roller {
-  public:
-    roller();
-    roller(engine::random && g);
-    
-    /**
-     * @param d Some dice roll structure, containing any number of dice sets 'NdM'
-     * as well as any number of roll modifiers (fixed numbers). Additionally,
-     * can contain a repetition parameter.
-     * @return A vector of actualized rolls, where `vector.size() == d.num`.
-     */
-    std::vector<dice_roll> operator()(dice const & d);
-  private:
-    engine::random gen;
-  };
+// Describe the actual result of rolling an arbitrary set of dice with mods
+struct dice_roll {
+  // Collapse this roll into its actual value
+  operator int() const;
+  // A vector of component roll results, each on representing a single NdM
+  // expression.
+  std::vector<die_roll> sub_rolls;
+  // A vector of every modifier attached to the system.
+  std::vector<mod> modifiers;
+  difficulty_class dc;
+};
 
+class roller {
+public:
+  roller();
+  roller(engine::random && g);
+  
   /**
-   * Print out the component elements of an actualized dice roll.
-   * Use instead `out << int(r)` to print the final summation.
+   * @param d Some dice roll structure, containing any number of dice sets 'NdM'
+   * as well as any number of roll modifiers (fixed numbers). Additionally,
+   * can contain a repetition parameter.
+   * @return A vector of actualized rolls, where `vector.size() == d.num`.
    */
-  std::ostream & operator<<(std::ostream & out, dice_roll const & r);
+  std::vector<dice_roll> operator()(dice const & d);
+private:
+  engine::random gen;
+};
+
+/**
+ * Print out the component elements of an actualized dice roll.
+ * Use instead `out << int(r)` to print the final summation.
+ */
+std::ostream & operator<<(std::ostream & out, dice_roll const & r);
 }

+ 13 - 14
src/die.cxx

@@ -8,23 +8,22 @@
 
 #include "dice-roll/die.h"
 
-#include <iostream>
-#include <sstream>
-
 #include "dice-roll/exception.h"
 
 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 "";
-    }
+
+int sgn(sign s) { return s == sign::MINUS ? -1 : 1; }
+
+mod::operator int() const { return sgn(sign) * value; }
+
+bool difficulty_class::operator()(int value) const {
+  switch (comp) {
+    case test::None: return true;
+    case test::Less: return value < against;
+    case test::LessOrEqual: return value <= against;
+    case test::Greater: return value > against;
+    case test::GreaterOrEqual: return value >= against;
   }
+}
 
-  mod::operator int() const { return sgn(sign) * value; }
 }

+ 6 - 6
src/exception.cxx

@@ -21,10 +21,10 @@ static std::string carrot(long long pos) {
 }
 
 namespace dice {
-  unexpected_token::unexpected_token(std::string const & reason, long long pos)
-  : std::runtime_error(reason), position(pos) {}
-  
-  std::string unexpected_token::pointer(long long backup_length) const {
-    return carrot(position == -1 ? backup_length : position);
-  }
+unexpected_token::unexpected_token(std::string const & reason, long long pos)
+    : std::runtime_error(reason), position(pos) {}
+
+std::string unexpected_token::pointer(long long backup_length) const {
+  return carrot(position == -1 ? backup_length : position);
+}
 }

+ 54 - 3
src/io.cxx

@@ -10,23 +10,74 @@
 #include <sstream>
 
 #include "dice-roll/die.h"
-#include "dice-roll/exception.h"
 #include "dice-roll/parser.h"
+#include "dice-roll/roll.h"
 
 namespace dice {
 
+std::ostream & operator<<(std::ostream & out, sign s) {
+  switch (s) {
+    case sign::PLUS: return out << '+';
+    case sign::MINUS: return out << '-';
+    case sign::ZERO: return out;
+  }
+}
+
+std::ostream & operator<<(std::ostream & out, difficulty_class::test t) {
+  switch (t) {
+    case difficulty_class::test::None: return out;
+    case difficulty_class::test::Less: return out << '<';
+    case difficulty_class::test::LessOrEqual: return out << '<' << '=';
+    case difficulty_class::test::Greater: return out << '>';
+    case difficulty_class::test::GreaterOrEqual: return out << '>' << '=';
+  }
+}
+
 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;
+    out << di.sgn << di.num << 'd' << di.sides;
   }
   for (mod m : d.modifier) {
-    out << str(m.sign) << m.value;
+    out << m.sign << m.value;
+  }
+  if (d.dc.comp != difficulty_class::test::None) {
+    out << d.dc.comp << d.dc.against;
   }
   if (d.num != 1) out << '}';
   return out;
 }
 
+std::ostream & operator<<(std::ostream & out, die_roll const & r) {
+  out << r.sign;
+  switch (r.rolled.size()) {
+  case 0:
+      // Prevent crashes if we somehow get a 0dM expression
+    return out << "0";
+  case 1:
+      // Don't bother with braces if there's only a single roll,
+      // the braces are for grouping purposes.
+    return out << r.rolled[0];
+  default:
+    out << "[ ";
+    out << r.rolled[0];
+    for (int i = 1; i < r.rolled.size(); ++i) {
+      out << ", " << r.rolled[i];
+    }
+    return out << " ]";
+  }
+}
+
+std::ostream & operator<<(std::ostream & out, dice_roll const & r) {
+  for (die_roll const & dr : r.sub_rolls) {
+    out << dr;
+  }
+  for (mod const & m : r.modifiers) {
+    out << m.sign << m.value;
+  }
+  return out;
+}
+
 std::istream & operator>>(std::istream & in, dice & d) {
   d = parser(in).parse();
   return in;

+ 26 - 5
src/parser.cxx

@@ -29,7 +29,7 @@ void parser::parse_dN(sign s, int value) {
     throw unexpected_token("Expected a number of sides", is_.tellg());
   }
   is_ >> dice_.of.back().sides;
-  parse_impl(ZERO);
+  parse_impl(sign::ZERO);
 }
 
 void parser::parse_const(sign s, int value) {
@@ -38,6 +38,23 @@ void parser::parse_const(sign s, int value) {
   }
 }
 
+void parser::parse_dc(char token) {
+  using test = difficulty_class::test;
+  bool const is_eq = is_.peek() == '=';
+  if (is_eq) { is_.get(); }
+  if (token == '<') {
+    dice_.dc.comp = is_eq ? test::LessOrEqual : test::Less;
+  } else {
+    dice_.dc.comp = is_eq ? test::GreaterOrEqual : test::Greater;
+  }
+  advance_over_whitespace(is_);
+  if (isnumber(is_.peek())) {
+    is_ >> dice_.dc.against;
+  } else {
+    throw unexpected_token("Expected number for DC", is_.tellg());
+  }
+}
+
 void parser::parse_impl(sign s) {
   advance_over_whitespace(is_);
   // By defaulting this to zero, we can write a more elegant handling of
@@ -45,7 +62,7 @@ void parser::parse_impl(sign s) {
   int value = 0;
   if (isnumber(is_.peek())) {
     is_ >> value;
-  } else if (is_.peek() == EOF && s != ZERO) {
+  } else if (is_.peek() == EOF && s != sign::ZERO) {
     throw unexpected_token("Unexpected EOF while parsing", -1);
   }
   advance_over_whitespace(is_);
@@ -58,7 +75,11 @@ void parser::parse_impl(sign s) {
     // Handle 5+... cases
     parse_const(s, value);
     // Add another token
-    parse_impl((is_.get() == '+') ? PLUS : MINUS);
+    parse_impl((is_.get() == '+') ? sign::PLUS : sign::MINUS);
+    break;
+  case '<':
+  case '>':
+    parse_dc(is_.get());
     break;
   default:
     parse_const(s, value);
@@ -76,13 +97,13 @@ dice parser::parse() {
   switch (is_.peek()) {
   case 'd':
   case 'D':
-    parse_impl(ZERO);
+    parse_impl(sign::ZERO);
     dice_.of.front().num = value;
     break;
   case '{':
     is_.get();
     dice_.num = value;
-    parse_impl(ZERO);
+    parse_impl(sign::ZERO);
     if (is_.get() != '}') {
       throw unexpected_token("Expected closing '}' in repeated roll",
                              is_.tellg());

+ 33 - 60
src/roll.cxx

@@ -8,78 +8,51 @@
 
 #include "dice-roll/roll.h"
 
-#include <iostream>
-#include <memory>
 #include <numeric>
 
 #include "dice-roll/random.h"
 
 namespace dice {
-  die_roll::operator int() const {
-    return sgn(sign) * std::accumulate(rolled.begin(), rolled.end(), 0);
-  }
-
-  dice_roll::operator int() const {
-    return std::accumulate(sub_rolls.begin(), sub_rolls.end(), 0) +
-           std::accumulate(modifiers.begin(), modifiers.end(), 0);
-  }
+die_roll::operator int() const {
+  return sgn(sign) * std::accumulate(rolled.begin(), rolled.end(), 0);
+}
 
-  std::ostream & operator<<(std::ostream & out, die_roll const & r) {
-    out << str(r.sign);
-    switch (r.rolled.size()) {
-    case 0:
-        // Prevent crashes if we somehow get a 0dM expression
-      return out << "0";
-    case 1:
-        // Don't bother with braces if there's only a single roll,
-        // the braces are for grouping purposes.
-      return out << r.rolled[0];
-    default:
-      out << "[ ";
-      out << r.rolled[0];
-      for (int i = 1; i < r.rolled.size(); ++i) {
-        out << ", " << r.rolled[i];
-      }
-      return out << " ]";
-    }
+dice_roll::operator int() const {
+  auto subtotal = std::accumulate(sub_rolls.begin(), sub_rolls.end(), 0) +
+         std::accumulate(modifiers.begin(), modifiers.end(), 0);
+  if (dc.comp != difficulty_class::test::None) {
+    return static_cast<int>(dc(subtotal));
+  } else {
+    return subtotal;
   }
+}
 
-  std::ostream & operator<<(std::ostream & out, dice_roll const & r) {
-    for (die_roll const & dr : r.sub_rolls) {
-      out << dr;
-    }
-    for (mod const & m : r.modifiers) {
-      out << str(m.sign) << m.value;
-    }
-    return out;
+die_roll roll_impl(die const & d, engine::random & gen) {
+  std::vector<int> hits;
+  for (int i = 0; i < d.num; ++i) {
+    hits.push_back(gen.roll(d.sides));
   }
+  return {d.sgn, hits};
+}
 
-  die_roll roll_impl(die const & d, engine::random & gen) {
-    std::vector<int> hits;
-    for (int i = 0; i < d.num; ++i) {
-      hits.push_back(gen.roll(d.sides));
-    }
-    return {d.sgn, hits};
+dice_roll roll_impl(dice const & d, engine::random & gen) {
+  std::vector<die_roll> hits;
+  for (die const & di : d.of) {
+    hits.push_back(roll_impl(di, gen));
   }
+  return {hits, d.modifier, d.dc};
+}
 
-  dice_roll roll_impl(dice const & d, engine::random & gen) {
-    std::vector<die_roll> hits;
-    for (die const & di : d.of) {
-      hits.push_back(roll_impl(di, gen));
-    }
-    return {hits, d.modifier};
-  }
-  
-  roller::roller() : gen() {}
-  roller::roller(engine::random && g)
-  : gen(std::forward<engine::random>(g)) {
-  }
+roller::roller() : gen() {}
+roller::roller(engine::random && g)
+: gen(std::forward<engine::random>(g)) {
+}
 
-  std::vector<dice_roll> roller::operator()(dice const & d) {
-    std::vector<dice_roll> out;
-    for (int i = 0; i < d.num; ++i) {
-      out.emplace_back(roll_impl(d, gen));
-    }
-    return out;
+std::vector<dice_roll> roller::operator()(dice const & d) {
+  std::vector<dice_roll> out;
+  for (int i = 0; i < d.num; ++i) {
+    out.emplace_back(roll_impl(d, gen));
   }
+  return out;
+}
 }

+ 25 - 25
src/terminal_helper.cxx

@@ -15,34 +15,34 @@
 #include "dice-roll/roll.h"
 
 namespace terminal { namespace {
-  void print(dice::dice_roll const & r) {
-    std::cout << int(r) << " (" << r << ")\n";
-  }
-  
-  void print(std::vector<dice::dice_roll> const & rs) {
-    if (rs.size() != 1) {
-      std::cout << '\n';
-      for (int i = 0; i < rs.size(); ++i) {
-        std::cout << "  Result/" << i << ": ";
-        print(rs[i]);
-      }
-    } else {
-      print(rs[0]);
+void print(dice::dice_roll const & r) {
+  std::cout << int(r) << " (" << r << ")\n";
+}
+
+void print(std::vector<dice::dice_roll> const & rs) {
+  if (rs.size() != 1) {
+    std::cout << '\n';
+    for (int i = 0; i < rs.size(); ++i) {
+      std::cout << "  Result/" << i << ": ";
+      print(rs[i]);
     }
+  } else {
+    print(rs[0]);
   }
+}
 }}
 
 namespace terminal {
-  void process_dice_string(std::string const & str) {
-    auto d = dice::from_string(str);
-    auto rs = dice::roller()(d);
-    std::cout << "Result of '" << d << "': ";
-    print(rs);
-  }
-  
-  void print_error_message(std::string const & str,
-                           dice::unexpected_token const & ut) {
-    std::cerr << "Error in roll: '" << str << "': " << ut.what() << "\n";
-    std::cerr << "                " << ut.pointer(str.size() + 1) << std::endl;
-  }
+void process_dice_string(std::string const & str) {
+  auto d = dice::from_string(str);
+  auto rs = dice::roller()(d);
+  std::cout << "Result of '" << d << "': ";
+  print(rs);
+}
+
+void print_error_message(std::string const & str,
+                         dice::unexpected_token const & ut) {
+  std::cerr << "Error in roll: '" << str << "': " << ut.what() << "\n";
+  std::cerr << "                " << ut.pointer(str.size() + 1) << std::endl;
+}
 }

+ 7 - 7
src/terminal_helper.h

@@ -11,14 +11,14 @@
 #include <string>
 
 namespace dice {
-  class unexpected_token;
+class unexpected_token;
 }
 
 namespace terminal {
-  /**
-   * @param str {@see make_dice(std::string const &)}
-   */
-  void process_dice_string(std::string const & str);
-  void print_error_message(std::string const & str,
-                           dice::unexpected_token const & ut);
+/**
+ * @param str {@see make_dice(std::string const &)}
+ */
+void process_dice_string(std::string const & str);
+void print_error_message(std::string const & str,
+                         dice::unexpected_token const & ut);
 }

+ 48 - 0
test/die_test.cxx

@@ -0,0 +1,48 @@
+//
+//  die_test.cxx
+//  dice-td
+//
+//  Created by Sam Jaffe on 1/16/21.
+//  Copyright © 2021 Sam Jaffe. All rights reserved.
+//
+
+#include "xcode_gtest_helper.h"
+
+#include "dice-roll/die.h"
+
+using test = dice::difficulty_class::test;
+
+TEST(DifficultyClassTest, NoneAlwaysReturnsTrue) {
+  dice::difficulty_class dc{test::None, 0};
+  for (int i = -100; i < 100; ++i) {
+    EXPECT_TRUE(dc(i));
+  }
+}
+
+TEST(DifficultyClassTest, LessThan) {
+  dice::difficulty_class dc{test::Less, 10};
+  EXPECT_TRUE(dc(9));
+  EXPECT_FALSE(dc(10));
+  EXPECT_FALSE(dc(11));
+}
+
+TEST(DifficultyClassTest, LessThanOrEqualTo) {
+  dice::difficulty_class dc{test::LessOrEqual, 10};
+  EXPECT_TRUE(dc(9));
+  EXPECT_TRUE(dc(10));
+  EXPECT_FALSE(dc(11));
+}
+
+TEST(DifficultyClassTest, GreaterThan) {
+  dice::difficulty_class dc{test::Greater, 10};
+  EXPECT_FALSE(dc(9));
+  EXPECT_FALSE(dc(10));
+  EXPECT_TRUE(dc(11));
+}
+
+TEST(DifficultyClassTest, GreaterThanOrEqualTo) {
+  dice::difficulty_class dc{test::GreaterOrEqual, 10};
+  EXPECT_FALSE(dc(9));
+  EXPECT_TRUE(dc(10));
+  EXPECT_TRUE(dc(11));
+}

+ 79 - 0
test/io_test.cxx

@@ -0,0 +1,79 @@
+//
+//  io_test.cxx
+//  dice-td
+//
+//  Created by Sam Jaffe on 1/16/21.
+//  Copyright © 2021 Sam Jaffe. All rights reserved.
+//
+
+#include <sstream>
+
+#include "xcode_gtest_helper.h"
+
+#include "dice-roll/die.h"
+#include "dice-roll/roll.h"
+
+using testing::Eq;
+
+TEST(DiceIOTest, StringFormIsExplicit) {
+  std::stringstream ss;
+  ss << dice::from_string("2{2d6-d4+5}");
+  EXPECT_THAT(ss.str(), Eq("2{2d6-1d4+5}"));
+}
+
+TEST(DiceIOTest, AllModifiersComeAtTheEnd) {
+  std::stringstream ss;
+  ss << dice::from_string("2d6-4-1d6+5");
+  EXPECT_THAT(ss.str(), Eq("2d6-1d6-4+5"));
+}
+
+TEST(DiceIOTest, StringFormDoesNotPreserveWhitespace) {
+  std::stringstream ss;
+  ss << dice::from_string("2 { d 4 + 5 }");
+  EXPECT_THAT(ss.str(), Eq("2{1d4+5}"));
+}
+
+TEST(DiceIOTest, CanReadDirectlyFromStream) {
+  std::stringstream in("2d6-4-1d6+5"), out;
+  dice::dice d;
+  in >> d;
+  out << d;
+  EXPECT_THAT(out.str(), Eq("2d6-1d6-4+5"));
+}
+
+TEST(DiceIOTest, PrintsOutDCMarker) {
+  std::string const tokens[] = { "<", "<=", ">", ">=" };
+  for (auto &tok : tokens) {
+    std::stringstream ss;
+    ss << dice::from_string("1d20" + tok + "10");
+    EXPECT_THAT(ss.str(), "1d20" + tok + "10");
+  }
+}
+
+TEST(RollIOTest, CanPrint0dN) {
+  dice::dice_roll roll{{{}}};
+  std::stringstream ss;
+  ss << roll;
+  EXPECT_THAT(ss.str(), Eq("0"));
+}
+
+TEST(RollIOTest, Prints1dNAsRollNumber) {
+  dice::dice_roll roll{{{dice::sign::ZERO, {3}}}};
+  std::stringstream ss;
+  ss << roll;
+  EXPECT_THAT(ss.str(), Eq("3"));
+}
+
+TEST(RollIOTest, PrintsNdMAsList) {
+  dice::dice_roll roll{{{dice::sign::ZERO, {3, 2, 4}}}};
+  std::stringstream ss;
+  ss << roll;
+  EXPECT_THAT(ss.str(), Eq("[ 3, 2, 4 ]"));
+}
+
+TEST(RollIOTest, PrintsModifiersAfterList) {
+  dice::dice_roll roll{{{dice::sign::ZERO, {3, 2, 4}}}, {{dice::sign::PLUS, 2}}};
+  std::stringstream ss;
+  ss << roll;
+  EXPECT_THAT(ss.str(), Eq("[ 3, 2, 4 ]+2"));
+}

+ 53 - 27
test/dice_test.cxx

@@ -15,29 +15,29 @@
 
 using unsigned_die = std::pair<int, int>;
 namespace dice {
-  bool operator==(die const & lhs, die const & rhs) {
-    return lhs.sgn == rhs.sgn && lhs.num == rhs.num && lhs.sides == rhs.sides;
-  }
-  
-  bool operator==(die const & lhs, unsigned_die const & rhs) {
-    return lhs.num == rhs.first && lhs.sides == rhs.second;
-  }
+bool operator==(die const & lhs, die const & rhs) {
+  return lhs.sgn == rhs.sgn && lhs.num == rhs.num && lhs.sides == rhs.sides;
+}
+
+bool operator==(die const & lhs, unsigned_die const & rhs) {
+  return lhs.num == rhs.first && lhs.sides == rhs.second;
+}
 }
 
 using namespace ::testing;
 
-TEST(DiceTest, ThrowsOnEmptyString) {
+TEST(ParserTest, ThrowsOnEmptyString) {
   EXPECT_THROW(dice::from_string(""), dice::unexpected_token);
 }
 
-TEST(DiceTest, ThrowsOnOpeningArithmetic) {
+TEST(ParserTest, ThrowsOnOpeningArithmetic) {
   EXPECT_THROW(dice::from_string("+5"), dice::unexpected_token);
   EXPECT_THROW(dice::from_string("-5"), dice::unexpected_token);
   EXPECT_THROW(dice::from_string("+1d4"), dice::unexpected_token);
   EXPECT_THROW(dice::from_string("-1d4"), dice::unexpected_token);
 }
 
-TEST(DiceTest, Implicit1dN) {
+TEST(ParserTest, Implicit1dN) {
   dice::dice capture;
   EXPECT_NO_THROW(capture = dice::from_string("d4"));
   EXPECT_THAT(capture.num, Eq(1));
@@ -45,7 +45,7 @@ TEST(DiceTest, Implicit1dN) {
   EXPECT_THAT(capture.of[0], Eq(unsigned_die(1, 4)));
 }
 
-TEST(DiceTest, Explicit1dN) {
+TEST(ParserTest, Explicit1dN) {
   dice::dice capture;
   EXPECT_NO_THROW(capture = dice::from_string("1d4"));
   EXPECT_THAT(capture.num, Eq(1));
@@ -53,11 +53,11 @@ TEST(DiceTest, Explicit1dN) {
   EXPECT_THAT(capture.of[0], Eq(unsigned_die(1, 4)));
 }
 
-TEST(DiceTest, CannotImplicitNumberOfSides) {
+TEST(ParserTest, CannotImplicitNumberOfSides) {
   EXPECT_THROW(dice::from_string("1d"), dice::unexpected_token);
 }
 
-TEST(DiceTest, AllowsMultipleDice) {
+TEST(ParserTest, AllowsMultipleDice) {
   dice::dice capture;
   EXPECT_NO_THROW(capture = dice::from_string("1d4+1d6"));
   EXPECT_THAT(capture.of, SizeIs(2));
@@ -65,7 +65,7 @@ TEST(DiceTest, AllowsMultipleDice) {
                                       Eq(unsigned_die(1, 6))));
 }
 
-TEST(DiceTest, CanIncludeConstant) {
+TEST(ParserTest, CanIncludeConstant) {
   dice::dice capture;
   EXPECT_NO_THROW(capture = dice::from_string("1d4+1"));
   EXPECT_THAT(capture.of, SizeIs(1));
@@ -73,7 +73,7 @@ TEST(DiceTest, CanIncludeConstant) {
   EXPECT_THAT(capture.modifier[0].value, 1);
 }
 
-TEST(DiceTest, ConstantSignIsInSgnMember) {
+TEST(ParserTest, ConstantSignIsInSgnMember) {
   dice::dice capture;
   EXPECT_NO_THROW(capture = dice::from_string("1d4-1"));
   EXPECT_THAT(capture.of, SizeIs(1));
@@ -81,22 +81,22 @@ TEST(DiceTest, ConstantSignIsInSgnMember) {
   EXPECT_THAT(capture.modifier[0].value, 1);
 }
 
-TEST(DiceTest, ThrowsIfUnterminatedArithmatic) {
+TEST(ParserTest, ThrowsIfUnterminatedArithmatic) {
   EXPECT_THROW(dice::from_string("1d4+"), dice::unexpected_token);
 }
 
-TEST(DiceTest, CanProduceMultiRollExpression) {
+TEST(ParserTest, CanProduceMultiRollExpression) {
   dice::dice capture;
   EXPECT_NO_THROW(capture = dice::from_string("2{d4}"));
   EXPECT_THAT(capture.num, Eq(2));
   EXPECT_THAT(capture.of, SizeIs(1));
 }
 
-TEST(DiceTest, MultiRollWillThrowIfNoEndBrace) {
+TEST(ParserTest, MultiRollWillThrowIfNoEndBrace) {
   EXPECT_THROW(dice::from_string("2{d4"), dice::unexpected_token);
 }
 
-TEST(DiceTest, IgnoresWhitespace) {
+TEST(ParserTest, IgnoresWhitespace) {
   dice::dice capture;
   EXPECT_NO_THROW(capture = dice::from_string("2 { d 4 + 5 }"));
   EXPECT_THAT(capture.num, Eq(2));
@@ -104,14 +104,40 @@ TEST(DiceTest, IgnoresWhitespace) {
   EXPECT_THAT(capture.modifier, SizeIs(1));
 }
 
-TEST(DiceSerialTest, StringFormIsExplicit) {
-  std::stringstream ss;
-  ss << dice::from_string("2{2d6-d4+5}");
-  EXPECT_THAT(ss.str(), Eq("2{2d6-1d4+5}"));
+TEST(ParserTest, CanParseDC) {
+  EXPECT_NO_THROW(dice::from_string("1d20<10"));
+  EXPECT_NO_THROW(dice::from_string("1d20<=10"));
+  EXPECT_NO_THROW(dice::from_string("1d20>10"));
+  EXPECT_NO_THROW(dice::from_string("1d20>=10"));
+}
+
+TEST(ParserTest, DCCannotBeNegative) {
+  EXPECT_THROW(dice::from_string("1d20>-1"), dice::unexpected_token);
+}
+
+TEST(ParserTest, DCCanBeWhitespacePadded) {
+  EXPECT_NO_THROW(dice::from_string("1d20 < 10"));
+}
+
+TEST(ParserTest, CanParseDCForMultiroll) {
+  EXPECT_NO_THROW(dice::from_string("2{1d20<10}"));
+  EXPECT_NO_THROW(dice::from_string("2{1d20<=10}"));
+  EXPECT_NO_THROW(dice::from_string("2{1d20>10}"));
+  EXPECT_NO_THROW(dice::from_string("2{1d20>=10}"));
+}
+
+TEST(ParserTest, MultirollDCMustAppearInsideBrackets) {
+  EXPECT_NO_THROW(dice::from_string("2{1d20}<10"));
+  EXPECT_NO_THROW(dice::from_string("2{1d20}<=10"));
+  EXPECT_NO_THROW(dice::from_string("2{1d20}>10"));
+  EXPECT_NO_THROW(dice::from_string("2{1d20}>=10"));
 }
 
-TEST(DiceSerialTest, StringFormDoesNotPreserveWhitespace) {
-  std::stringstream ss;
-  ss << dice::from_string("2 { d 4 + 5 }");
-  EXPECT_THAT(ss.str(), Eq("2{1d4+5}"));
+TEST(ParserTest, DCIsCaptured) {
+  using test = dice::difficulty_class::test;
+  EXPECT_THAT(dice::from_string("1d20<10").dc.against, 10);
+  EXPECT_THAT(dice::from_string("1d20<10").dc.comp, test::Less);
+  EXPECT_THAT(dice::from_string("1d20<=10").dc.comp, test::LessOrEqual);
+  EXPECT_THAT(dice::from_string("1d20>10").dc.comp, test::Greater);
+  EXPECT_THAT(dice::from_string("1d20>=10").dc.comp, test::GreaterOrEqual);
 }

+ 31 - 29
test/roll_test.cxx

@@ -19,7 +19,7 @@ struct MockRandomImpl : public engine::detail::random_impl {
   double inclusive(double, double) { throw; }
 };
 
-class RollTest : public ::testing::Test {
+class RollerTest : public ::testing::Test {
 protected:
   void SetUp() override {
     pRandom = std::make_shared<MockRandomImpl>();
@@ -35,13 +35,13 @@ protected:
 
 using namespace ::testing;
 
-TEST_F(RollTest, CanConstructNoThrow) {
+TEST_F(RollerTest, CanConstructNoThrow) {
   EXPECT_NO_THROW(dice::roller{});
   EXPECT_NO_THROW(dice::roller{pRandom});
 }
 
-TEST_F(RollTest, RollsOncePerDie) {
-  dice::dice mydice{1, {{dice::ZERO, 3, 4}}, {{dice::PLUS, 1}}};
+TEST_F(RollerTest, RollsOncePerDie) {
+  dice::dice mydice{1, {{dice::sign::ZERO, 3, 4}}, {{dice::sign::PLUS, 1}}};
   EXPECT_CALL(*pRandom, exclusive(4)).WillOnce(Return(2)).WillOnce(Return(1))
       .WillOnce(Return(3));
   
@@ -51,8 +51,8 @@ TEST_F(RollTest, RollsOncePerDie) {
   EXPECT_THAT(roll[0].sub_rolls[0].rolled, ElementsAre(3, 2, 4));
 }
 
-TEST_F(RollTest, DoesNotRollForConstants) {
-  dice::dice mydice{1, {}, {{dice::PLUS, 1}}};
+TEST_F(RollerTest, DoesNotRollForConstants) {
+  dice::dice mydice{1, {}, {{dice::sign::PLUS, 1}}};
   EXPECT_CALL(*pRandom, exclusive(_)).Times(0);
   
   auto roll = dice::roller{pRandom}(mydice);
@@ -62,33 +62,35 @@ TEST_F(RollTest, DoesNotRollForConstants) {
   EXPECT_THAT(roll[0].modifiers[0].value, Eq(1));
 }
 
-TEST_F(RollTest, OperatorInSumsElements) {
-  dice::dice mydice{1, {{dice::ZERO, 3, 4}}, {{dice::PLUS, 1}}};
+TEST_F(RollerTest, InjectsDCIntoRollObjects) {
+  using test = dice::difficulty_class::test;
+  dice::dice mydice{1, {{dice::sign::ZERO, 3, 4}},
+      {{dice::sign::PLUS, 1}}, {test::Less, 20}};
   EXPECT_CALL(*pRandom, exclusive(4)).WillOnce(Return(2)).WillOnce(Return(1))
-  .WillOnce(Return(3));
-  
-  auto roll = dice::roller{pRandom}(mydice);
-  // 3 + 2 + 4 + 1 = 10
-  EXPECT_THAT(int(roll[0]), Eq(10));
-}
+      .WillOnce(Return(3));
 
-TEST(RollSerialTest, CanPrint0dN) {
-  dice::dice_roll roll{{{}}};
-  std::stringstream ss;
-  ss << roll;
-  EXPECT_THAT(ss.str(), Eq("0"));
+  auto roll = dice::roller{pRandom}(mydice);
+  EXPECT_THAT(roll, Each(Field(&dice::dice_roll::dc,
+                               Field(&dice::difficulty_class::against, 20))));
 }
 
-TEST(RollSerialTest, Prints1dNAsRollNumber) {
-  dice::dice_roll roll{{{dice::ZERO, {3}}}};
-  std::stringstream ss;
-  ss << roll;
-  EXPECT_THAT(ss.str(), Eq("3"));
+TEST(DiceRollTest, OperatorIntSumsElements) {
+  dice::dice_roll const roll{
+    {{dice::sign::ZERO, {3, 2, 4}}},
+    {{dice::sign::PLUS, 1}}};
+  // 3 + 2 + 4 + 1 = 10
+  EXPECT_THAT(int(roll), Eq(10));
 }
 
-TEST(RollSerialTest, PrintsNdMAsList) {
-  dice::dice_roll roll{{{dice::ZERO, {3, 2, 4}}}};
-  std::stringstream ss;
-  ss << roll;
-  EXPECT_THAT(ss.str(), Eq("[ 3, 2, 4 ]"));
+TEST(DiceRollTest, OperatorIntWithDCReturnsOneOrZero) {
+  using test = dice::difficulty_class::test;
+  dice::dice_roll roll{
+    {{dice::sign::ZERO, {3, 2, 4}}},
+    {{dice::sign::PLUS, 1}},
+    {test::Less, 10}
+  };
+  
+  EXPECT_THAT(int(roll), Eq(0));
+  --roll.modifiers[0].value;
+  EXPECT_THAT(int(roll), Eq(1));
 }