A set of classes for parsing, evaluating, and formatting die roll strings.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SA_DiceEvaluator.m 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. //
  2. // SA_DiceEvaluator.m
  3. //
  4. // Copyright 2016-2021 Said Achmiz.
  5. // See LICENSE and README.md for more info.
  6. #import "SA_DiceEvaluator.h"
  7. #import "SA_DiceBag.h"
  8. #import "SA_DiceParser.h"
  9. #import "SA_DiceExpression.h"
  10. #import "SA_DiceExpressionStringConstants.h"
  11. #import "SA_Utility.h"
  12. /**************************/
  13. #pragma mark Defined values
  14. /**************************/
  15. #define DEFAULT_MAX_DIE_COUNT 1000 // One thousand
  16. #define DEFAULT_MAX_DIE_SIZE 10000 // Ten thousand
  17. /***************************************************/
  18. #pragma mark - SA_DiceEvaluator class implementation
  19. /***************************************************/
  20. @implementation SA_DiceEvaluator {
  21. SA_DiceBag *_diceBag;
  22. NSUInteger _maxDieCount;
  23. NSUInteger _maxDieSize;
  24. }
  25. /************************/
  26. #pragma mark - Properties
  27. /************************/
  28. -(NSUInteger) maxDieCount {
  29. return _maxDieCount;
  30. }
  31. -(void) setMaxDieCount:(NSUInteger)maxDieCount {
  32. if (maxDieCount > (NSUIntegerMax / _maxDieSize)) {
  33. _maxDieCount = NSUIntegerMax / _maxDieSize;
  34. } else {
  35. _maxDieCount = maxDieCount;
  36. }
  37. }
  38. -(NSUInteger) maxDieSize {
  39. return _maxDieSize;
  40. }
  41. -(void) setMaxDieSize:(NSUInteger)maxDieSize {
  42. if ( maxDieSize > [_diceBag biggestPossibleDieSize]
  43. || maxDieSize > (NSIntegerMax / _maxDieCount)) {
  44. _maxDieSize = (([_diceBag biggestPossibleDieSize] < (NSIntegerMax / _maxDieCount))
  45. ? [_diceBag biggestPossibleDieSize]
  46. : (NSIntegerMax / _maxDieCount));
  47. } else {
  48. _maxDieSize = maxDieSize;
  49. }
  50. }
  51. /**************************/
  52. #pragma mark - Initializers
  53. /**************************/
  54. -(instancetype) init {
  55. if (!(self = [super init]))
  56. return nil;
  57. _maxDieCount = DEFAULT_MAX_DIE_COUNT;
  58. _maxDieSize = DEFAULT_MAX_DIE_SIZE;
  59. _diceBag = [SA_DiceBag new];
  60. return self;
  61. }
  62. /****************************/
  63. #pragma mark - Public methods
  64. /****************************/
  65. // TODO: Possibly refuse to evaluate an expression that’s already evaluated?
  66. // (i.e., it has a ... .result?? .value??)
  67. -(SA_DiceExpression *) resultOfExpression:(SA_DiceExpression *)expression {
  68. // Check to see if the expression is erroneous (i.e. the parser has judged
  69. // that it is malformed, etc.). If so, decline to evaluate the expression;
  70. // return (a copy of) it, unchanged.
  71. if (expression.errorBitMask != 0) {
  72. return [expression copy];
  73. }
  74. /*
  75. NOTE: Even if an expression is not erroneous (i.e. if it has no syntax
  76. errors), it may still not be possible to evaluate it. For example, ‘5d0’
  77. is a perfectly well-formed die string, and will yield an expression tree
  78. as follows:
  79. [ type : SA_DiceExpressionTerm_ROLL_COMMAND,
  80. rollCommand : SA_DiceExpressionRollCommand_SUM,
  81. dieCount : [ type : SA_DiceExpressionTerm_VALUE,
  82. value : @(5)
  83. ],
  84. dieSize : [ type : SA_DiceExpressionTerm_VALUE,
  85. value : @(0)
  86. ]
  87. ]
  88. This is, of course, an illegal expression; we can’t roll a die of size 0
  89. (a die with zero sides?).
  90. If we encounter such an illegal expression, we add an appropriate error to
  91. the -[errorBitMask]. We are not required to set a value (-[value] property)
  92. in such a case.
  93. */
  94. switch (expression.type) {
  95. case SA_DiceExpressionTerm_OPERATION: {
  96. return [self resultOfExpressionDescribingOperation:expression];
  97. break;
  98. }
  99. case SA_DiceExpressionTerm_ROLL_COMMAND: {
  100. return [self resultOfExpressionDescribingRollCommand:expression];
  101. break;
  102. }
  103. case SA_DiceExpressionTerm_ROLL_MODIFIER: {
  104. return [self resultOfExpressionDescribingRollModifier:expression];
  105. break;
  106. }
  107. case SA_DiceExpressionTerm_VALUE:
  108. default: {
  109. return [self resultOfExpressionDescribingValue:expression];
  110. break;
  111. }
  112. }
  113. }
  114. /****************************/
  115. #pragma mark - Helper methods
  116. /****************************/
  117. -(SA_DiceExpression *) resultOfExpressionDescribingValue:(SA_DiceExpression *)expression {
  118. SA_DiceExpression *result = [expression copy];
  119. result.result = result.value;
  120. return result;
  121. }
  122. -(SA_DiceExpression *) resultOfExpressionDescribingRollCommand:(SA_DiceExpression *)expression {
  123. SA_DiceExpression *result = [expression copy];
  124. // For now, only sum and exploding sum (i.e., sum but with exploding dice)
  125. // are supported. Other sorts of roll commands may be added later.
  126. switch (result.rollCommand) {
  127. case SA_DiceExpressionRollCommand_SUM:
  128. case SA_DiceExpressionRollCommand_SUM_EXPLODING: {
  129. // First, recursively evaluate the expressions that represent the
  130. // die count and (for standard dice) the die size.
  131. result.dieCount = [self resultOfExpression:result.dieCount];
  132. if (result.dieType == SA_DiceExpressionDice_STANDARD)
  133. result.dieSize = [self resultOfExpression:result.dieSize];
  134. // Evaluating those expressions may have generated an error(s);
  135. // propagate any errors up to the current term.
  136. result.errorBitMask |= result.dieCount.errorBitMask;
  137. if (result.dieType == SA_DiceExpressionDice_STANDARD)
  138. result.errorBitMask |= result.dieSize.errorBitMask;
  139. // If indeed we’ve turned up errors, return.
  140. if (result.errorBitMask != 0)
  141. return result;
  142. // Evaluating the two sub-expressions didn’t generate errors; this means
  143. // that we have successfully generated values for both the die count and
  144. // (for standard dice) the die size...
  145. NSInteger dieCount = result.dieCount.result.integerValue;
  146. NSInteger dieSize = 0;
  147. if (result.dieType == SA_DiceExpressionDice_STANDARD)
  148. dieSize = result.dieSize.result.integerValue;
  149. // ... but, the resulting values of the expressions may make it
  150. // impossible to evaluate the roll command. Check to see whether the die
  151. // count and die size have legal values.
  152. if (dieCount < 0) {
  153. result.errorBitMask |= SA_DiceExpressionError_DIE_COUNT_NEGATIVE;
  154. } else if (dieCount > _maxDieCount) {
  155. result.errorBitMask |= SA_DiceExpressionError_DIE_COUNT_EXCESSIVE;
  156. }
  157. // Die type only matters for standard dice, not for Fudge dice.
  158. if (result.dieType == SA_DiceExpressionDice_STANDARD) {
  159. if (dieSize < 1) {
  160. result.errorBitMask |= SA_DiceExpressionError_DIE_SIZE_INVALID;
  161. } else if (dieSize > _maxDieSize) {
  162. result.errorBitMask |= SA_DiceExpressionError_DIE_SIZE_EXCESSIVE;
  163. }
  164. }
  165. // If indeed the die count or die size fall outside of their allowed
  166. // ranges, return.
  167. if (result.errorBitMask != 0)
  168. return result;
  169. // The die count and die size have legal values. We can safely roll the
  170. // requisite number of dice, and take the sum of the rolls (if needed).
  171. // NOTE: _maxDieSize is guaranteed to be no greater than the largest die
  172. // size that the SA_DiceBag can roll (this is enforced by the setter
  173. // method for the maxDieSize property), so we need not check to see
  174. // if the return value of rollDie: or rollNumber:ofDice: is valid.
  175. // We are also guaranteed that the product of _maxDieCount and
  176. // _maxDieSize is no greater than the largest unsigned value that can be
  177. // stored by whatever numeric type we specify simple value terms (terms
  178. // of type SA_DiceExpressionTerm_VALUE) to contain (likewise enforced
  179. // by the setters for both maxDieSize and maxDieCount), therefore we
  180. // need not worry about overflow here.
  181. if (dieCount == 0) {
  182. result.result = @(0);
  183. result.rolls = @[];
  184. } else {
  185. NSArray *rolls;
  186. if (result.dieType == SA_DiceExpressionDice_STANDARD) {
  187. SA_DiceRollingOptions options = (result.rollCommand == SA_DiceExpressionRollCommand_SUM_EXPLODING) ? SA_DiceRollingExplodingDice : 0;
  188. rolls = [_diceBag rollNumber:dieCount
  189. ofDice:dieSize
  190. withOptions:options];
  191. } else if (result.dieType == SA_DiceExpressionDice_FUDGE) {
  192. rolls = [_diceBag rollFudgeDice:dieCount];
  193. }
  194. result.result = [rolls valueForKeyPath:@"@sum.self"];
  195. result.rolls = rolls;
  196. }
  197. break;
  198. }
  199. default: {
  200. result.errorBitMask |= SA_DiceExpressionError_UNKNOWN_ROLL_COMMAND;
  201. break;
  202. }
  203. }
  204. return result;
  205. }
  206. -(SA_DiceExpression *) resultOfExpressionDescribingRollModifier:(SA_DiceExpression *)expression {
  207. SA_DiceExpression *result = [expression copy];
  208. switch (result.rollModifier) {
  209. case SA_DiceExpressionRollModifier_KEEP_HIGHEST:
  210. case SA_DiceExpressionRollModifier_KEEP_LOWEST: {
  211. // These roll modifiers takes the highest, or the lowest, N rolls
  212. // out of all the rolls generated by a roll command, discarding the
  213. // rest, and summing the kept ones.
  214. // First, check if the left-hand operand is a roll command (and
  215. // specifically, a simple sum; though this latter requirement may
  216. // be dropped later).
  217. // TODO: re-evaluate this ^
  218. // If the left-hand operand is not a roll-and-sum, then the KEEP
  219. // modifier cannot be applied to it. In that case, we add an error
  220. // and return the result without evaluating.
  221. if ( result.leftOperand.type != SA_DiceExpressionTerm_ROLL_COMMAND
  222. || ( result.leftOperand.rollCommand != SA_DiceExpressionRollCommand_SUM
  223. && result.leftOperand.rollCommand != SA_DiceExpressionRollCommand_SUM_EXPLODING)
  224. ) {
  225. result.errorBitMask |= SA_DiceExpressionError_ROLL_MODIFIER_INAPPLICABLE;
  226. return result;
  227. }
  228. // We now know the left-hand operand is a roll command. Recursively
  229. // evaluate the expressions that represent the roll command and the
  230. // modifier value (the right-hand operand).
  231. result.leftOperand = [self resultOfExpression:result.leftOperand];
  232. result.rightOperand = [self resultOfExpression:result.rightOperand];
  233. // Evaluating the operands may have generated an error(s); propagate any
  234. // errors up to the current term.
  235. result.errorBitMask |= result.leftOperand.errorBitMask;
  236. result.errorBitMask |= result.rightOperand.errorBitMask;
  237. // If indeed we’ve turned up errors, return.
  238. if (result.errorBitMask != 0)
  239. return result;
  240. // Evaluating the operands didn’t generate any errors; this means
  241. // that on the left hand we have a set of rolls (as well as a
  242. // result, which we are ignoring), and on the right hand we have a
  243. // result, which specifies how many rolls to keep.
  244. NSArray <NSNumber *> *rolls = result.leftOperand.rolls;
  245. NSNumber *keepHowMany = result.rightOperand.result;
  246. // However, it is now possible that the “keep how many” value
  247. // exceeds the number of rolls, which would make the expression
  248. // incoherent. If so, add an error and return.
  249. if (keepHowMany.unsignedIntegerValue > rolls.count) {
  250. result.errorBitMask |= SA_DiceExpressionError_KEEP_COUNT_EXCEEDS_ROLL_COUNT;
  251. return result;
  252. }
  253. // It is also possible that the “keep how many” value is negative.
  254. // This, too, would make the expression incoherent. Likewise, add
  255. // an error and return.
  256. if (keepHowMany < 0) {
  257. result.errorBitMask |= SA_DiceExpressionError_KEEP_COUNT_NEGATIVE;
  258. return result;
  259. }
  260. // We sort the rolls array...
  261. BOOL sortAscending = (result.rollModifier == SA_DiceExpressionRollModifier_KEEP_LOWEST);
  262. result.rolls = [rolls sortedArrayUsingDescriptors:@[ [NSSortDescriptor sortDescriptorWithKey:@"integerValue"
  263. ascending:sortAscending] ]];
  264. // And the ‘result’ property of the result expression is the sum of
  265. // the first <keepHowMany> elements of the sorted rolls array.
  266. result.result = [[result.rolls subarrayWithRange:NSRangeMake(0, keepHowMany.unsignedIntegerValue)] valueForKeyPath:@"@sum.self"];
  267. break;
  268. }
  269. default: {
  270. result.errorBitMask |= SA_DiceExpressionError_UNKNOWN_ROLL_MODIFIER;
  271. break;
  272. }
  273. }
  274. return result;
  275. }
  276. -(SA_DiceExpression *) resultOfExpressionDescribingOperation:(SA_DiceExpression *)expression {
  277. SA_DiceExpression *result = [expression copy];
  278. // First, recursively evaluate the expressions that represent the
  279. // left-hand-side and right-hand-side operands.
  280. result.leftOperand = [self resultOfExpression:result.leftOperand];
  281. result.rightOperand = [self resultOfExpression:result.rightOperand];
  282. // Evaluating the operands may have generated an error(s); propagate any
  283. // errors up to the current term.
  284. result.errorBitMask |= result.leftOperand.errorBitMask;
  285. result.errorBitMask |= result.rightOperand.errorBitMask;
  286. // If indeed we’ve turned up errors, return.
  287. if (result.errorBitMask != 0)
  288. return result;
  289. // Evaluating the operands didn’t generate any errors. We have valid
  290. // operands.
  291. NSInteger leftOperand = result.leftOperand.result.integerValue;
  292. NSInteger rightOperand = result.rightOperand.result.integerValue;
  293. switch (result.operator) {
  294. case SA_DiceExpressionOperator_MINUS: {
  295. // First, we check for possible overflow...
  296. if ( leftOperand > 0
  297. && rightOperand < 0
  298. && (NSIntegerMax + rightOperand) < leftOperand) {
  299. result.errorBitMask |= SA_DiceExpressionError_INTEGER_OVERFLOW_SUBTRACTION;
  300. break;
  301. } else if ( leftOperand < 0
  302. && rightOperand > 0
  303. && (NSIntegerMin + rightOperand) > leftOperand) {
  304. result.errorBitMask |= SA_DiceExpressionError_INTEGER_UNDERFLOW_SUBTRACTION;
  305. break;
  306. }
  307. // No overflow will occur. We can perform the subtraction operation.
  308. result.result = @(leftOperand - rightOperand);
  309. break;
  310. }
  311. case SA_DiceExpressionOperator_PLUS: {
  312. // First, we check for possible overflow...
  313. if ( rightOperand > 0
  314. && leftOperand > 0
  315. && (NSIntegerMax - rightOperand) < leftOperand) {
  316. result.errorBitMask |= SA_DiceExpressionError_INTEGER_OVERFLOW_ADDITION;
  317. break;
  318. } else if ( rightOperand < 0
  319. && leftOperand < 0
  320. && (NSIntegerMin - rightOperand) > leftOperand) {
  321. result.errorBitMask |= SA_DiceExpressionError_INTEGER_UNDERFLOW_ADDITION;
  322. break;
  323. }
  324. // No overflow will occur. We can perform the addition operation.
  325. result.result = @(leftOperand + rightOperand);
  326. break;
  327. }
  328. case SA_DiceExpressionOperator_TIMES: {
  329. // First, we check for possible overflow...
  330. if ( ( leftOperand == NSIntegerMin
  331. && ( rightOperand != 0
  332. || rightOperand != 1 ))
  333. || ( rightOperand == NSIntegerMin
  334. && ( leftOperand != 0
  335. || leftOperand != 1 ))
  336. || ( leftOperand != 0
  337. && ((NSIntegerMax / ABS(leftOperand)) < rightOperand))
  338. ) {
  339. if ( ( leftOperand > 0
  340. && rightOperand > 0)
  341. || ( leftOperand < 0
  342. && rightOperand < 0)) {
  343. result.errorBitMask |= SA_DiceExpressionError_INTEGER_OVERFLOW_MULTIPLICATION;
  344. } else {
  345. result.errorBitMask |= SA_DiceExpressionError_INTEGER_UNDERFLOW_MULTIPLICATION;
  346. }
  347. break;
  348. }
  349. // No overflow will occur. We can perform the multiplication operation.
  350. result.result = @(leftOperand * rightOperand);
  351. break;
  352. }
  353. default: {
  354. // We add the appropriate error. We do not set a value for the
  355. // result property.
  356. result.errorBitMask |= SA_DiceExpressionError_UNKNOWN_OPERATOR;
  357. break;
  358. }
  359. }
  360. return result;
  361. }
  362. @end