| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- //
- // SA_DiceEvaluator.m
- //
- // Copyright 2016-2021 Said Achmiz.
- // See LICENSE and README.md for more info.
-
- #import "SA_DiceEvaluator.h"
-
- #import "SA_DiceBag.h"
- #import "SA_DiceParser.h"
- #import "SA_DiceExpression.h"
- #import "SA_DiceExpressionStringConstants.h"
-
- #import "SA_Utility.h"
-
- /**************************/
- #pragma mark Defined values
- /**************************/
-
- #define DEFAULT_MAX_DIE_COUNT 1000 // One thousand
- #define DEFAULT_MAX_DIE_SIZE 10000 // Ten thousand
-
- /***************************************************/
- #pragma mark - SA_DiceEvaluator class implementation
- /***************************************************/
-
- @implementation SA_DiceEvaluator {
- SA_DiceBag *_diceBag;
-
- NSUInteger _maxDieCount;
- NSUInteger _maxDieSize;
- }
-
- /************************/
- #pragma mark - Properties
- /************************/
-
- -(NSUInteger) maxDieCount {
- return _maxDieCount;
- }
- -(void) setMaxDieCount:(NSUInteger)maxDieCount {
- if (maxDieCount > (NSUIntegerMax / _maxDieSize)) {
- _maxDieCount = NSUIntegerMax / _maxDieSize;
- } else {
- _maxDieCount = maxDieCount;
- }
- }
-
- -(NSUInteger) maxDieSize {
- return _maxDieSize;
- }
- -(void) setMaxDieSize:(NSUInteger)maxDieSize {
- if ( maxDieSize > [_diceBag biggestPossibleDieSize]
- || maxDieSize > (NSIntegerMax / _maxDieCount)) {
- _maxDieSize = (([_diceBag biggestPossibleDieSize] < (NSIntegerMax / _maxDieCount))
- ? [_diceBag biggestPossibleDieSize]
- : (NSIntegerMax / _maxDieCount));
- } else {
- _maxDieSize = maxDieSize;
- }
- }
-
- /**************************/
- #pragma mark - Initializers
- /**************************/
-
- -(instancetype) init {
- if (!(self = [super init]))
- return nil;
-
- _maxDieCount = DEFAULT_MAX_DIE_COUNT;
- _maxDieSize = DEFAULT_MAX_DIE_SIZE;
-
- _diceBag = [SA_DiceBag new];
-
- return self;
- }
-
- /****************************/
- #pragma mark - Public methods
- /****************************/
-
- // TODO: Possibly refuse to evaluate an expression that’s already evaluated?
- // (i.e., it has a ... .result?? .value??)
- -(SA_DiceExpression *) resultOfExpression:(SA_DiceExpression *)expression {
- // Check to see if the expression is erroneous (i.e. the parser has judged
- // that it is malformed, etc.). If so, decline to evaluate the expression;
- // return (a copy of) it, unchanged.
- if (expression.errorBitMask != 0) {
- return [expression copy];
- }
-
- /*
- NOTE: Even if an expression is not erroneous (i.e. if it has no syntax
- errors), it may still not be possible to evaluate it. For example, ‘5d0’
- is a perfectly well-formed die string, and will yield an expression tree
- as follows:
-
- [ type : SA_DiceExpressionTerm_ROLL_COMMAND,
- rollCommand : SA_DiceExpressionRollCommand_SUM,
- dieCount : [ type : SA_DiceExpressionTerm_VALUE,
- value : @(5)
- ],
- dieSize : [ type : SA_DiceExpressionTerm_VALUE,
- value : @(0)
- ]
- ]
-
- This is, of course, an illegal expression; we can’t roll a die of size 0
- (a die with zero sides?).
-
- If we encounter such an illegal expression, we add an appropriate error to
- the -[errorBitMask]. We are not required to set a value (-[value] property)
- in such a case.
- */
-
- switch (expression.type) {
- case SA_DiceExpressionTerm_OPERATION: {
- return [self resultOfExpressionDescribingOperation:expression];
- break;
- }
- case SA_DiceExpressionTerm_ROLL_COMMAND: {
- return [self resultOfExpressionDescribingRollCommand:expression];
- break;
- }
- case SA_DiceExpressionTerm_ROLL_MODIFIER: {
- return [self resultOfExpressionDescribingRollModifier:expression];
- break;
- }
- case SA_DiceExpressionTerm_VALUE:
- default: {
- return [self resultOfExpressionDescribingValue:expression];
- break;
- }
- }
- }
-
- /****************************/
- #pragma mark - Helper methods
- /****************************/
-
- -(SA_DiceExpression *) resultOfExpressionDescribingValue:(SA_DiceExpression *)expression {
- SA_DiceExpression *result = [expression copy];
-
- result.result = result.value;
-
- return result;
- }
-
- -(SA_DiceExpression *) resultOfExpressionDescribingRollCommand:(SA_DiceExpression *)expression {
- SA_DiceExpression *result = [expression copy];
-
- // For now, only sum and exploding sum (i.e., sum but with exploding dice)
- // are supported. Other sorts of roll commands may be added later.
- switch (result.rollCommand) {
- case SA_DiceExpressionRollCommand_SUM:
- case SA_DiceExpressionRollCommand_SUM_EXPLODING: {
- // First, recursively evaluate the expressions that represent the
- // die count and (for standard dice) the die size.
- result.dieCount = [self resultOfExpression:result.dieCount];
- if (result.dieType == SA_DiceExpressionDice_STANDARD)
- result.dieSize = [self resultOfExpression:result.dieSize];
-
- // Evaluating those expressions may have generated an error(s);
- // propagate any errors up to the current term.
- result.errorBitMask |= result.dieCount.errorBitMask;
- if (result.dieType == SA_DiceExpressionDice_STANDARD)
- result.errorBitMask |= result.dieSize.errorBitMask;
-
- // If indeed we’ve turned up errors, return.
- if (result.errorBitMask != 0)
- return result;
-
- // Evaluating the two sub-expressions didn’t generate errors; this means
- // that we have successfully generated values for both the die count and
- // (for standard dice) the die size...
- NSInteger dieCount = result.dieCount.result.integerValue;
- NSInteger dieSize = 0;
- if (result.dieType == SA_DiceExpressionDice_STANDARD)
- dieSize = result.dieSize.result.integerValue;
-
- // ... but, the resulting values of the expressions may make it
- // impossible to evaluate the roll command. Check to see whether the die
- // count and die size have legal values.
- if (dieCount < 0) {
- result.errorBitMask |= SA_DiceExpressionError_DIE_COUNT_NEGATIVE;
- } else if (dieCount > _maxDieCount) {
- result.errorBitMask |= SA_DiceExpressionError_DIE_COUNT_EXCESSIVE;
- }
-
- // Die type only matters for standard dice, not for Fudge dice.
- if (result.dieType == SA_DiceExpressionDice_STANDARD) {
- if (dieSize < 1) {
- result.errorBitMask |= SA_DiceExpressionError_DIE_SIZE_INVALID;
- } else if (dieSize > _maxDieSize) {
- result.errorBitMask |= SA_DiceExpressionError_DIE_SIZE_EXCESSIVE;
- }
- }
-
- // If indeed the die count or die size fall outside of their allowed
- // ranges, return.
- if (result.errorBitMask != 0)
- return result;
-
- // The die count and die size have legal values. We can safely roll the
- // requisite number of dice, and take the sum of the rolls (if needed).
- // NOTE: _maxDieSize is guaranteed to be no greater than the largest die
- // size that the SA_DiceBag can roll (this is enforced by the setter
- // method for the maxDieSize property), so we need not check to see
- // if the return value of rollDie: or rollNumber:ofDice: is valid.
- // We are also guaranteed that the product of _maxDieCount and
- // _maxDieSize is no greater than the largest unsigned value that can be
- // stored by whatever numeric type we specify simple value terms (terms
- // of type SA_DiceExpressionTerm_VALUE) to contain (likewise enforced
- // by the setters for both maxDieSize and maxDieCount), therefore we
- // need not worry about overflow here.
- if (dieCount == 0) {
- result.result = @(0);
- result.rolls = @[];
- } else {
- NSArray *rolls;
- if (result.dieType == SA_DiceExpressionDice_STANDARD) {
- SA_DiceRollingOptions options = (result.rollCommand == SA_DiceExpressionRollCommand_SUM_EXPLODING) ? SA_DiceRollingExplodingDice : 0;
- rolls = [_diceBag rollNumber:dieCount
- ofDice:dieSize
- withOptions:options];
- } else if (result.dieType == SA_DiceExpressionDice_FUDGE) {
- rolls = [_diceBag rollFudgeDice:dieCount];
- }
-
- result.result = [rolls valueForKeyPath:@"@sum.self"];
- result.rolls = rolls;
- }
-
- break;
- }
- default: {
- result.errorBitMask |= SA_DiceExpressionError_UNKNOWN_ROLL_COMMAND;
-
- break;
- }
- }
-
- return result;
- }
-
- -(SA_DiceExpression *) resultOfExpressionDescribingRollModifier:(SA_DiceExpression *)expression {
- SA_DiceExpression *result = [expression copy];
-
- switch (result.rollModifier) {
- case SA_DiceExpressionRollModifier_KEEP_HIGHEST:
- case SA_DiceExpressionRollModifier_KEEP_LOWEST: {
- // These roll modifiers takes the highest, or the lowest, N rolls
- // out of all the rolls generated by a roll command, discarding the
- // rest, and summing the kept ones.
-
- // First, check if the left-hand operand is a roll command (and
- // specifically, a simple sum; though this latter requirement may
- // be dropped later).
- // TODO: re-evaluate this ^
- // If the left-hand operand is not a roll-and-sum, then the KEEP
- // modifier cannot be applied to it. In that case, we add an error
- // and return the result without evaluating.
- if ( result.leftOperand.type != SA_DiceExpressionTerm_ROLL_COMMAND
- || ( result.leftOperand.rollCommand != SA_DiceExpressionRollCommand_SUM
- && result.leftOperand.rollCommand != SA_DiceExpressionRollCommand_SUM_EXPLODING)
- ) {
- result.errorBitMask |= SA_DiceExpressionError_ROLL_MODIFIER_INAPPLICABLE;
- return result;
- }
-
- // We now know the left-hand operand is a roll command. Recursively
- // evaluate the expressions that represent the roll command and the
- // modifier value (the right-hand operand).
- result.leftOperand = [self resultOfExpression:result.leftOperand];
- result.rightOperand = [self resultOfExpression:result.rightOperand];
-
- // Evaluating the operands may have generated an error(s); propagate any
- // errors up to the current term.
- result.errorBitMask |= result.leftOperand.errorBitMask;
- result.errorBitMask |= result.rightOperand.errorBitMask;
-
- // If indeed we’ve turned up errors, return.
- if (result.errorBitMask != 0)
- return result;
-
- // Evaluating the operands didn’t generate any errors; this means
- // that on the left hand we have a set of rolls (as well as a
- // result, which we are ignoring), and on the right hand we have a
- // result, which specifies how many rolls to keep.
- NSArray <NSNumber *> *rolls = result.leftOperand.rolls;
- NSNumber *keepHowMany = result.rightOperand.result;
-
- // However, it is now possible that the “keep how many” value
- // exceeds the number of rolls, which would make the expression
- // incoherent. If so, add an error and return.
- if (keepHowMany.unsignedIntegerValue > rolls.count) {
- result.errorBitMask |= SA_DiceExpressionError_KEEP_COUNT_EXCEEDS_ROLL_COUNT;
- return result;
- }
-
- // It is also possible that the “keep how many” value is negative.
- // This, too, would make the expression incoherent. Likewise, add
- // an error and return.
- if (keepHowMany < 0) {
- result.errorBitMask |= SA_DiceExpressionError_KEEP_COUNT_NEGATIVE;
- return result;
- }
-
- // We sort the rolls array...
- BOOL sortAscending = (result.rollModifier == SA_DiceExpressionRollModifier_KEEP_LOWEST);
- result.rolls = [rolls sortedArrayUsingDescriptors:@[ [NSSortDescriptor sortDescriptorWithKey:@"integerValue"
- ascending:sortAscending] ]];
-
- // And the ‘result’ property of the result expression is the sum of
- // the first <keepHowMany> elements of the sorted rolls array.
- result.result = [[result.rolls subarrayWithRange:NSRangeMake(0, keepHowMany.unsignedIntegerValue)] valueForKeyPath:@"@sum.self"];
-
- break;
- }
- default: {
- result.errorBitMask |= SA_DiceExpressionError_UNKNOWN_ROLL_MODIFIER;
-
- break;
- }
- }
-
- return result;
- }
-
- -(SA_DiceExpression *) resultOfExpressionDescribingOperation:(SA_DiceExpression *)expression {
- SA_DiceExpression *result = [expression copy];
-
- // First, recursively evaluate the expressions that represent the
- // left-hand-side and right-hand-side operands.
- result.leftOperand = [self resultOfExpression:result.leftOperand];
- result.rightOperand = [self resultOfExpression:result.rightOperand];
-
- // Evaluating the operands may have generated an error(s); propagate any
- // errors up to the current term.
- result.errorBitMask |= result.leftOperand.errorBitMask;
- result.errorBitMask |= result.rightOperand.errorBitMask;
-
- // If indeed we’ve turned up errors, return.
- if (result.errorBitMask != 0)
- return result;
-
- // Evaluating the operands didn’t generate any errors. We have valid
- // operands.
- NSInteger leftOperand = result.leftOperand.result.integerValue;
- NSInteger rightOperand = result.rightOperand.result.integerValue;
-
- switch (result.operator) {
- case SA_DiceExpressionOperator_MINUS: {
- // First, we check for possible overflow...
- if ( leftOperand > 0
- && rightOperand < 0
- && (NSIntegerMax + rightOperand) < leftOperand) {
- result.errorBitMask |= SA_DiceExpressionError_INTEGER_OVERFLOW_SUBTRACTION;
- break;
- } else if ( leftOperand < 0
- && rightOperand > 0
- && (NSIntegerMin + rightOperand) > leftOperand) {
- result.errorBitMask |= SA_DiceExpressionError_INTEGER_UNDERFLOW_SUBTRACTION;
- break;
- }
-
- // No overflow will occur. We can perform the subtraction operation.
- result.result = @(leftOperand - rightOperand);
- break;
- }
- case SA_DiceExpressionOperator_PLUS: {
- // First, we check for possible overflow...
- if ( rightOperand > 0
- && leftOperand > 0
- && (NSIntegerMax - rightOperand) < leftOperand) {
- result.errorBitMask |= SA_DiceExpressionError_INTEGER_OVERFLOW_ADDITION;
- break;
- } else if ( rightOperand < 0
- && leftOperand < 0
- && (NSIntegerMin - rightOperand) > leftOperand) {
- result.errorBitMask |= SA_DiceExpressionError_INTEGER_UNDERFLOW_ADDITION;
- break;
- }
-
- // No overflow will occur. We can perform the addition operation.
- result.result = @(leftOperand + rightOperand);
- break;
- }
- case SA_DiceExpressionOperator_TIMES: {
- // First, we check for possible overflow...
- if ( ( leftOperand == NSIntegerMin
- && ( rightOperand != 0
- || rightOperand != 1 ))
- || ( rightOperand == NSIntegerMin
- && ( leftOperand != 0
- || leftOperand != 1 ))
- || ( leftOperand != 0
- && ((NSIntegerMax / ABS(leftOperand)) < rightOperand))
- ) {
- if ( ( leftOperand > 0
- && rightOperand > 0)
- || ( leftOperand < 0
- && rightOperand < 0)) {
- result.errorBitMask |= SA_DiceExpressionError_INTEGER_OVERFLOW_MULTIPLICATION;
- } else {
- result.errorBitMask |= SA_DiceExpressionError_INTEGER_UNDERFLOW_MULTIPLICATION;
- }
- break;
- }
-
- // No overflow will occur. We can perform the multiplication operation.
- result.result = @(leftOperand * rightOperand);
- break;
- }
- default: {
- // We add the appropriate error. We do not set a value for the
- // result property.
- result.errorBitMask |= SA_DiceExpressionError_UNKNOWN_OPERATOR;
- break;
- }
- }
-
- return result;
- }
-
- @end
|