├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── README.rst ├── composer.json ├── demo.php ├── phpunit.xml.dist ├── src └── DiceCalc │ ├── Calc.php │ ├── CalcDice.php │ ├── CalcOperation.php │ ├── CalcSet.php │ └── Random.php └── tests ├── DiceCalc └── Tests │ ├── Calc │ ├── BasicRollTest.php │ ├── BasicRollWithMathTest.php │ ├── ConditionNumberTest.php │ ├── CustomDieTest.php │ ├── KeepPostfixTest.php │ ├── LowestHighestTest.php │ ├── MathTest.php │ └── TargetNumberTest.php │ └── CalcOperation │ ├── AddTest.php │ ├── CalcTest.php │ ├── DevideTest.php │ ├── EqualToTest.php │ ├── ExponentTest.php │ ├── GreaterThanTest.php │ ├── LessThanTest.php │ ├── MultiplyTest.php │ ├── NaNTest.php │ ├── ReduceTest.php │ └── SubtractTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | composer.phar 3 | /vendor/ 4 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - php 3 | 4 | tools: 5 | # PHP 6 | php_sim: true 7 | php_code_sniffer: true 8 | php_hhvm: true 9 | php_mess_detector: true 10 | php_pdepend: true 11 | php_loc: true 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.4 4 | - 5.5 5 | - 5.6 6 | - 7.0 7 | - hhvm 8 | 9 | branches: 10 | only: 11 | - master 12 | - dev 13 | 14 | script: phpunit --coverage-text 15 | 16 | install: composer install 17 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | DiceCalc 3 | ======== 4 | 5 | Project status 6 | -------------- 7 | 8 | .. image:: https://travis-ci.org/ringmaster/dicecalc.svg?branch=master 9 | :target: https://travis-ci.org/ringmaster/dicecalc 10 | 11 | .. image:: https://scrutinizer-ci.com/g/ringmaster/dicecalc/badges/quality-score.png?b=master 12 | :target: https://scrutinizer-ci.com/g/ringmaster/dicecalc/?branch=master 13 | 14 | How to use 15 | ---------- 16 | Create a new Calc class, and pass the dice expression to the constructor:: 17 | $calc = new Calc($expression); 18 | 19 | Output the interpretation of the roll:: 20 | echo $calc->infix(); 21 | 22 | Output the result of the roll:: 23 | echo $calc(); 24 | 25 | Dice Expressions 26 | ---------------- 27 | The basic expression is to use a "d" followed by the number of sides of the die to roll. 28 | 29 | *Examples:* 30 | 31 | d6 32 | roll a six-sided die 33 | d20 34 | roll a twenty-sided die 35 | d% 36 | roll a percentile (1-100) die 37 | df 38 | roll a fudge (-1, 0, 1) die 39 | 40 | To roll multiple of the same sided die, prefix the die type with a multiplier number. 41 | 42 | *Examples:* 43 | 44 | 3d6 45 | roll d6 three times, sum the result 46 | 2d20 47 | roll d20 twice, sum the result 48 | 6df 49 | roll six fudge dice, sum the result 50 | 51 | Basic math operations are possible as expressions. 52 | 53 | *Examples:* 54 | 55 | 4+4 56 | Add 4+4 57 | 4*5 58 | Multiply 4*5 59 | 4+4*5 60 | Multiply 4*5, then add 4 (order of operations is obeyed) 61 | (4+4)*5 62 | Add 4+4, then multiply by 5 63 | 64 | Math on sets of numbers. 65 | 66 | *Examples:* 67 | 68 | 2*[3,4] 69 | Multiply 2*3 and 2*4, resulting in a set [6,8] 70 | [1,2]*[3,4] 71 | Multiply these sets together, resulting in a nested set [[3,4],[6,8]] 72 | 73 | Any combination of dice or constants in an expressions. 74 | 75 | *Examples:* 76 | 77 | 2d6+4 78 | roll 2d6, sum the dice and add 4 79 | 3d8-5 80 | roll 3d8, sum the dice and subtract 5 81 | 2d4+d6 82 | roll 2d4 and d6, sum all the dice 83 | 84 | Evaluate whether a result meets a certain condition or target number. 85 | 86 | *Examples:* 87 | 88 | 4+4 > 7 89 | returns true, because 4+4 is greater than 7 90 | 5d6 > 5 91 | returns true, because 5d6 is always greater than 5 92 | d20+4 > 12 93 | simulates a d20 system skill value of 4 compared to a target number of 12 94 | 95 | Produce a set of rolls by surrounding the roll with brackets and prefixing with a multiplier. 96 | 97 | *Examples:* 98 | 99 | 6[3d6] 100 | produces a set of 6 numbers created by rolling 3d6 101 | 4[d%] 102 | produces a set of 4 numbers created by rolling percentile 103 | 104 | Roll a custom die by specifying the dice sides in brackets after the "d". 105 | 106 | *Examples:* 107 | 108 | d[1,2,2,3,3,4] 109 | Roll a six-sided die with custom numeric faces 110 | d[red,green,blue] 111 | Roll a three-sided die with colored faces 112 | 113 | Keep only the dice in an individual roll that meet specific conditions by applying the "k" postfix. 114 | 115 | *Examples:* 116 | 117 | 5d6 keep > 4 118 | Role five six-sided dice, keep only the dice that are greater than 4, sum the kept dice 119 | 5d6k>4 120 | same as above 121 | 6d8 keep < 4 122 | Roll six eight-sided dice, keep only the dice that are less than 4, sum the kept dice 123 | 6d8k<4 124 | same as above 125 | 126 | Keep only the dice that are the lowest or highest values rolled. 127 | 128 | *Examples:* 129 | 130 | 4d6 highest 3 131 | Roll 4d6, keep only the highest 3 dice 132 | 4d6h3 133 | same as above 134 | 2s20 lowest 1 135 | Roll 2d20, keep only the lowest die 136 | 2d20l1 137 | same as above 138 | 139 | Reroll dice that do not meet certain conditions. 140 | 141 | *Examples:* 142 | 143 | 3d6 reroll < 3 144 | Roll 3d6, reroll any die that is less than 3 145 | 3d6r<3 146 | same as above 147 | 2d% reroll < 40 148 | Roll 2d%, reroll any die that is less than 40 149 | 2d%r<40 150 | same as above 151 | 152 | Our stupid way of rolling D&D character stats. 153 | 154 | *Examples:* 155 | 156 | 4d6r<3h3 157 | Roll 4d6, reroll any die that is less than 3, keep the highest 3 dice, sum the kept dice 158 | 159 | Produce open-ended dice using the "o" prefix. 160 | 161 | *Examples:* 162 | 163 | 4d6o=6 164 | Roll 4d6. When any die lands on 6, roll that die again and add the result to that die. Sum all die totals. 165 | 166 | A stupid example nobody would ever use, I hope: 167 | 168 | 3d6r<4o=6k>6 169 | Roll 3d6. Reroll any die less than 4. When any die is a 6, reroll and add the new value to the original one. Sum the die totals of only those dice that are greater than 6. 170 | 171 | Future Enhancements 172 | ------------------- 173 | * Better group handling 174 | * Better custom die handling 175 | * Variable replacements (To handle rolls like: d20 + $str_bonus > $target ) 176 | * Range violation exceptions (d6k<0) 177 | * Non-text output method 178 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ringmaster/dicecalc", 3 | "description": "DiceCalc", 4 | "version": "1.0.0", 5 | "keywords": ["dice", "dice calc", "dice calculator", "dice roller"], 6 | "license": "MIT", 7 | "require-dev": { 8 | "phpunit/phpunit": "@stable", 9 | "friendsofphp/php-cs-fixer": "@stable", 10 | "phpmd/phpmd": "@stable", 11 | "phploc/phploc": "@stable", 12 | "pdepend/pdepend": "@stable", 13 | "sebastian/phpcpd": "@stable", 14 | "sebastian/phpdcd": "@stable", 15 | "squizlabs/php_codesniffer": "@stable", 16 | "jakub-onderka/php-parallel-lint": "^0.9.2" 17 | }, 18 | "authors": [ 19 | { 20 | "name": "Owen Winkler", 21 | "email": "a_github@midnightcircus.com" 22 | } 23 | ], 24 | "require": { 25 | "php": ">=5.4" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "DiceCalc\\": "src/DiceCalc" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": {"DiceCalc\\": "tests/DiceCalc"} 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /demo.php: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | Calc Test 20 | 21 | 22 | 23 |
24 | 25 | 26 |
27 | 41 | 42 | 43 |
44 |   infix();
48 |   echo " => ";
49 |   echo $calc() . "\n";
50 | 
51 |   ?>
52 | 
53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | ./tests/DiceCalc/Tests 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/DiceCalc/Calc.php: -------------------------------------------------------------------------------- 1 | 10 | * @license MIT http://opensource.org/licenses/MIT 11 | */ 12 | class Calc { 13 | 14 | const DICE_REGEX = '(?P\d*)d(?P\d+|f|\%|\[[^\]]+\]) 15 | ( 16 | (?Pk(?:eep)?(?P[<>])(?P\d+)) 17 | | 18 | (?Pl(?:owest)?(?P\d+)) 19 | | 20 | (?Ph(?:ighest)?(?P\d+)) 21 | | 22 | (?Pr(?:eroll)?(?P[<>])(?P\d+)) 23 | | 24 | (?Po(?:pen)?(?P[<>=])(?P\d+)) 25 | | 26 | (?P[z]+) 27 | )*'; 28 | 29 | /** 30 | * @var array $ooo A list of operators with comparative order of operations 31 | */ 32 | private $ooo = [ 33 | '>' => 0, 34 | '<' => 0, 35 | '=' => 0, 36 | '-' => 10, 37 | '+' => 10, 38 | '*' => 20, 39 | '/' => 20, 40 | '^' => 30, 41 | ]; 42 | 43 | protected $expression; 44 | protected $rpn = []; 45 | protected $infix = []; 46 | 47 | protected $stack = []; 48 | 49 | /** 50 | * Create a dice calculation 51 | * 52 | * @param string $expression An expression to calculate 53 | */ 54 | public function __construct($expression = '') { 55 | $this->expression = str_replace(' ', '', $expression); 56 | 57 | preg_match_all('%(?: 58 | (?P' . self::DICE_REGEX . ') 59 | | 60 | (?P\d*\[[^\]]+\]) 61 | | 62 | (?P[\d\.]+) 63 | | 64 | (?P[+\-*^><=/]) 65 | | 66 | (?P\$[a-z_]+) 67 | | 68 | (?P[()]) 69 | )%ix', $this->expression, $matches, PREG_SET_ORDER); 70 | 71 | $this->stack = []; 72 | 73 | foreach ($matches as $match) { 74 | $match = array_filter($match, function ($value) { 75 | return $value !== false && $value !== ''; 76 | }); 77 | 78 | if (isset($match['numeral'])) { 79 | $this->match_numeral($match['numeral']); 80 | } elseif (isset($match['dice'])) { 81 | $this->match_dice($match['dice']); 82 | } elseif (isset($match['set'])) { 83 | $this->match_set($match['set']); 84 | } elseif (isset($match['operator'])) { 85 | $this->match_operator($match['operator']); 86 | } elseif (isset($match['variable'])) { 87 | $this->match_variable($match['variable']); 88 | } elseif (isset($match['parens'])) { 89 | $this->match_parens($match['parens']); 90 | } else { 91 | $this->stack = ['Invalid token:', $match]; 92 | break; 93 | } 94 | } 95 | 96 | $this->clear_stack(); 97 | } 98 | 99 | /** 100 | * @param int $numeral Numeral to add to the RPN stack 101 | */ 102 | protected function match_numeral($numeral) { 103 | $this->rpn[] = $numeral; 104 | $this->infix[] = $numeral; 105 | } 106 | 107 | protected function match_dice($dice) { 108 | $dice = new CalcDice($dice); 109 | $this->rpn[] = $dice->value(); 110 | $this->infix[] = $dice; 111 | } 112 | 113 | protected function match_set($set) { 114 | $this->rpn[] = new CalcSet($set); 115 | $this->infix[] = end($this->rpn); 116 | } 117 | 118 | /** 119 | * @param $variable 120 | */ 121 | protected function match_variable($variable) { 122 | $this->rpn[] = $variable; 123 | $this->infix[] = end($this->rpn); 124 | } 125 | 126 | protected function match_parens($parenthesis) { 127 | $this->infix[] = $parenthesis; 128 | if ($parenthesis == '(') { 129 | $this->stack[] = $parenthesis; 130 | } else { 131 | while (count($this->stack) > 0 && end($this->stack) != '(') { 132 | $this->rpn[] = array_pop($this->stack); 133 | } 134 | array_pop($this->stack); 135 | } 136 | } 137 | 138 | /** 139 | * @param $operator 140 | */ 141 | protected function match_operator($operator) { 142 | while ( 143 | count($this->stack) > 0 144 | && 145 | end($this->stack) != '(' 146 | && 147 | $this->ooo[$operator] <= $this->ooo[end($this->stack)] 148 | ) { 149 | $this->rpn[] = array_pop($this->stack); 150 | } 151 | $this->stack[] = $operator; 152 | $this->infix[] = $operator; 153 | } 154 | 155 | protected function clear_stack() { 156 | while (count($this->stack) > 0) { 157 | $this->rpn[] = array_pop($this->stack); 158 | } 159 | } 160 | 161 | /** 162 | * @return mixed|string 163 | * @throws \Exception 164 | */ 165 | public function __invoke() { 166 | 167 | $stack = []; 168 | 169 | foreach ($this->rpn as $step) { 170 | if (is_object($step) || !isset($this->ooo[$step])) { 171 | $stack[] = $step; 172 | } else { 173 | $r1 = array_pop($stack); 174 | $r2 = array_pop($stack); 175 | 176 | if (is_numeric($r1) && is_numeric($r2)) { 177 | $stack[] = CalcOperation::calc($step, $r2, $r1); 178 | } 179 | if ($r1 instanceof CalcSet && is_numeric($r2)) { 180 | $stack[] = $r1->calc($step, $r2); 181 | } 182 | if (is_numeric($r1) && $r2 instanceof CalcSet) { 183 | $stack[] = $r2->rcalc($step, $r1); 184 | } 185 | if ($r1 instanceof CalcSet && $r2 instanceof CalcSet) { 186 | $stack[] = $r1->mcalc($step, $r2); 187 | } 188 | } 189 | } 190 | 191 | if (count($stack) > 1) { 192 | throw new \Exception('Missing operator near "' . $stack[1] . '".'); 193 | } else { 194 | $out = reset($stack); 195 | 196 | return $out; 197 | } 198 | } 199 | 200 | public function infix() { 201 | return implode(' ', $this->infix); 202 | } 203 | 204 | } 205 | -------------------------------------------------------------------------------- /src/DiceCalc/CalcDice.php: -------------------------------------------------------------------------------- 1 | 10 | * @license MIT http://opensource.org/licenses/MIT 11 | */ 12 | class CalcDice extends CalcSet 13 | { 14 | protected $saved_values = []; 15 | 16 | public function __construct($set_value) 17 | { 18 | $this->values = []; 19 | $this->label = $set_value; 20 | preg_match('/' . Calc::DICE_REGEX . '/ix', $set_value, $matches); 21 | if (intval($matches['multiple']) == 0 && $matches['multiple'] != '0') { 22 | $matches['multiple'] = 1; 23 | } 24 | $matches += ['matches'=>'', 'openroll' => '', 'reroll' => '', 'keep' => '']; 25 | for ($z = 0; $z < $matches['multiple']; $z++) { 26 | $keep = true; 27 | 28 | $newval = $this->rolltype($matches['dietype']); 29 | 30 | if ($matches['reroll'] != '') { 31 | $gtlt = $matches['rerolleval']; 32 | $range = intval($matches['rerolllimit']); 33 | if ($gtlt == '<' && $newval < $range) { 34 | $keep = false; 35 | $z--; 36 | } 37 | if ($gtlt == '>' && $newval > $range) { 38 | $keep = false; 39 | $z--; 40 | } 41 | } 42 | 43 | if ($matches['openroll'] != '') { 44 | $gtlt = $matches['openrolleval']; 45 | $range = intval($matches['openrolllimit']); 46 | $addvals = [$newval]; 47 | $addval = $newval; 48 | 49 | $evals = [ 50 | '<' => function($addval, $range) { return $addval < $range; }, 51 | '>' => function($addval, $range) { return $addval > $range; }, 52 | '=' => function($addval, $range) { return $addval == $range; }, 53 | ]; 54 | 55 | while(isset($evals[$gtlt]) && $evals[$gtlt]($addval, $range)) { 56 | $addval = $this->rolltype($matches['dietype']); 57 | $addvals[] = $addval; 58 | } 59 | 60 | $newval = new Calc(implode('+', $addvals)); 61 | $newval = $newval(); 62 | } 63 | 64 | if ($keep) { 65 | $this->values['_' . count($this->values)] = $newval; 66 | } 67 | } 68 | 69 | $this->saved_values = $this->values; 70 | 71 | if ($matches['keep'] != '') { 72 | $gtlt = $matches['keepeval']; 73 | $range = intval($matches['keeprange']); 74 | foreach ($this->values as $k => $set_value) { 75 | $av = $set_value instanceof Calc ? $set_value() : $set_value; 76 | if ($gtlt == '>' && $av <= $range) { 77 | unset($this->values[$k]); 78 | } 79 | if ($gtlt == '<' && $av >= $range) { 80 | unset($this->values[$k]); 81 | } 82 | } 83 | } 84 | 85 | asort($this->values); 86 | if (isset($matches['highdice']) && $matches['highdice'] != '') { 87 | $this->values = array_slice($this->values, -intval($matches['highdice']), null, true); 88 | } 89 | if (isset($matches['lowdice']) && $matches['lowdice'] != '') { 90 | $this->values = array_slice($this->values, 0, intval($matches['lowdice']), true); 91 | } 92 | } 93 | 94 | public function rolltype($dietype) 95 | { 96 | if (is_numeric($dietype)) { 97 | $newval = Random::get(1, $dietype); 98 | } elseif ($dietype == 'f') { 99 | $newval = Random::get(-1, 1); 100 | } elseif ($dietype == '%') { 101 | $newval = Random::get(1, 100); 102 | } elseif ($dietype[0] == '[') { 103 | $dietype = trim($dietype, '[]'); 104 | $opts = explode(',', $dietype); 105 | $newval = $opts[Random::get(0, count($opts)-1)]; 106 | } else { 107 | var_dump($dietype); 108 | $newval = 'unknown'; 109 | } 110 | 111 | return $newval; 112 | } 113 | 114 | public function __toString() 115 | { 116 | $out = []; 117 | foreach (array_keys($this->saved_values) as $key) { 118 | $vout = $this->saved_values[$key]; 119 | if ($vout === true) { 120 | $vout = 'true'; 121 | } 122 | if ($vout === false) { 123 | $vout = 'false'; 124 | } 125 | if (isset($this->values[$key])) { 126 | $out[] = $vout; 127 | } else { 128 | if ($vout instanceof Calc) { 129 | $out[] = '' . $vout() . ''; 130 | } else { 131 | $out[] = '' . $vout . ''; 132 | } 133 | } 134 | } 135 | 136 | $result = '[' . $this->label . ':'; 137 | $comma = ''; 138 | foreach ($out as $o) { 139 | if ($o instanceof Calc) { 140 | $result .= $comma . $o(); 141 | } else { 142 | $result .= $comma . $o; 143 | } 144 | $comma = ' + '; 145 | } 146 | $result .= ']'; 147 | 148 | return $result; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/DiceCalc/CalcOperation.php: -------------------------------------------------------------------------------- 1 | 10 | * @license MIT http://opensource.org/licenses/MIT 11 | */ 12 | class CalcOperation 13 | { 14 | 15 | static protected $operators = [ 16 | '+' => 'add', 17 | '*' => 'multiply', 18 | '=' => 'equalto', 19 | '<' => 'lessthan', 20 | '>' => 'greaterthan', 21 | '^' => 'exponent', 22 | '/' => 'divide', 23 | '-' => 'subtract', 24 | ]; 25 | 26 | /** 27 | * @param string $operator 28 | * @param $operand2 29 | * @param $operand1 30 | * 31 | * @throws \Exception 32 | * @return bool|number 33 | */ 34 | public static function calc($operator, $operand1, $operand2) 35 | { 36 | if(isset(static::$operators[$operator])) { 37 | return call_user_func(array('self', static::$operators[$operator]), self::reduce($operand1), self::reduce($operand2)); 38 | } 39 | throw new \Exception('Unknown operator "' . $operator . '".'); 40 | } 41 | 42 | /** 43 | * @param $operand 44 | * 45 | * @return number|string 46 | * @throws \Exception 47 | */ 48 | public static function reduce($operand) 49 | { 50 | if(is_numeric($operand)) { 51 | return $operand; 52 | } 53 | elseif ($operand instanceof Calc) { 54 | return $operand(); 55 | } 56 | throw new \Exception('This is not a number'); 57 | } 58 | 59 | /** 60 | * @param number $operand1 61 | * @param number $operand2 62 | * 63 | * @return bool|number 64 | */ 65 | protected static function add($operand1, $operand2) 66 | { 67 | return $operand1 + $operand2; 68 | } 69 | 70 | /** 71 | * @param number $operand1 72 | * @param number $operand2 73 | * 74 | * @return bool|number 75 | */ 76 | protected static function multiply($operand1, $operand2) 77 | { 78 | return $operand1 * $operand2; 79 | } 80 | 81 | /** 82 | * @param number $operand1 83 | * @param number $operand2 84 | * 85 | * @return bool|number 86 | */ 87 | protected static function subtract($operand1, $operand2) 88 | { 89 | return $operand1 - $operand2; 90 | } 91 | 92 | /** 93 | * @param number $operand1 94 | * @param number $operand2 95 | * 96 | * @return bool|number 97 | */ 98 | protected static function divide($operand1, $operand2) 99 | { 100 | return $operand1 / $operand2; 101 | } 102 | 103 | /** 104 | * @param number $operand1 105 | * @param number $operand2 106 | * 107 | * @return bool|number 108 | */ 109 | protected static function exponent($operand1, $operand2) 110 | { 111 | return pow($operand1, $operand2); 112 | } 113 | 114 | /** 115 | * @param number $operand1 116 | * @param number $operand2 117 | * 118 | * @return bool 119 | */ 120 | protected static function greaterthan($operand1, $operand2) 121 | { 122 | return $operand1 > $operand2; 123 | } 124 | 125 | /** 126 | * @param number $operand1 127 | * @param number $operand2 128 | * 129 | * @return bool 130 | */ 131 | protected static function lessthan($operand1, $operand2) 132 | { 133 | return ($operand1 < $operand2); 134 | } 135 | 136 | /** 137 | * @param number $operand1 138 | * @param number $operand2 139 | * 140 | * @return bool 141 | */ 142 | protected static function equalto($operand1, $operand2) 143 | { 144 | return ($operand1 == $operand2); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/DiceCalc/CalcSet.php: -------------------------------------------------------------------------------- 1 | 10 | * @license MIT http://opensource.org/licenses/MIT 11 | */ 12 | class CalcSet { 13 | protected $values = []; 14 | protected $label; 15 | 16 | public function __construct($set_value) { 17 | if (is_array($set_value)) { 18 | $this->values = $set_value; 19 | $this->saved_values = $this->values; 20 | $this->label = '[' . implode(', ', $set_value) . ']'; 21 | } else { 22 | $this->label = $set_value; 23 | preg_match('%^(?P\d*)\[(?P[^\]]+)\]$%i', $set_value, $matches); 24 | $set_value = $matches['set']; 25 | $values = explode(',', $set_value); 26 | for ($z = 0; $z < max(1, intval($matches['multiple'])); $z ++) { 27 | foreach ($values as $set_value) { 28 | $this->values[] = $set_value; 29 | } 30 | } 31 | $this->saved_values = $this->values; 32 | foreach ($this->values as $k => $set_value) { 33 | $calc = new Calc($set_value); 34 | $this->values[$k] = $calc(); 35 | } 36 | foreach ($this->values as $k => $value) { 37 | $calc = new Calc($value); 38 | $this->values[$k] = $calc(); 39 | } 40 | } 41 | } 42 | 43 | public function __toString() { 44 | $out = []; 45 | foreach (array_keys($this->saved_values) as $key) { 46 | $vout = $this->values[$key]; 47 | if (!isset($this->values[$key])) { 48 | $vout = $this->saved_values[$key]; 49 | } 50 | if ($vout === true) { 51 | $vout = 'true'; 52 | } 53 | if ($vout === false) { 54 | $vout = 'false'; 55 | } 56 | if (isset($this->values[$key])) { 57 | $out[] = $vout; 58 | } else { 59 | $out[] = '' . $vout . ''; 60 | } 61 | } 62 | $out = '[' . implode(', ', $out) . ']'; 63 | 64 | return $out; 65 | } 66 | 67 | public function calc($operator, $operand) { 68 | $out = []; 69 | foreach ($this->values as $value) { 70 | $out[] = CalcOperation::calc($operator, $operand, $value); 71 | } 72 | 73 | return new CalcSet($out); 74 | } 75 | 76 | public function rcalc($operator, $operand) { 77 | $out = []; 78 | foreach ($this->values as $value) { 79 | $out[] = CalcOperation::calc($operator, $value, $operand); 80 | } 81 | 82 | return new CalcSet($out); 83 | } 84 | 85 | public function mcalc($operator, $operand) 86 | { 87 | $out = []; 88 | foreach ($this->values as $value) { 89 | $out[] = $operand->rcalc($operator, $value); 90 | } 91 | 92 | return new CalcSet($out); 93 | } 94 | 95 | public function value() 96 | { 97 | $allnumeric = true; 98 | foreach ($this->values as $v) { 99 | if (is_numeric($v)) { 100 | } else { 101 | $allnumeric = false; 102 | } 103 | } 104 | 105 | if ($allnumeric) { 106 | return array_sum($this->values); 107 | } else { 108 | return $this; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/DiceCalc/Random.php: -------------------------------------------------------------------------------- 1 | 10 | * @license MIT http://opensource.org/licenses/MIT 11 | */ 12 | class Random { 13 | public static $queue = null; 14 | public static $queue_list = []; 15 | 16 | public static function get($min, $max) { 17 | if(is_callable(self::$queue)) { 18 | $test_fn = self::$queue; 19 | $result = $test_fn($min, $max); 20 | } 21 | else { 22 | $result = mt_rand($min, $max); 23 | } 24 | return $result; 25 | } 26 | 27 | public static function set_queue($queue) { 28 | self::$queue = $queue; 29 | } 30 | 31 | public static function clear_queue() { 32 | self::$queue = null; 33 | } 34 | 35 | public static function queue_up() { 36 | $numbers = func_get_args(); 37 | self::$queue_list = self::$queue_list + $numbers; 38 | self::set_queue(function($min, $max){ 39 | return array_shift(self::$queue_list); 40 | }); 41 | } 42 | 43 | public static function queue_min() { 44 | self::set_queue(function($min, $max) { 45 | return $min; 46 | }); 47 | } 48 | 49 | public static function queue_max() { 50 | self::set_queue(function($min, $max) { 51 | return $max; 52 | }); 53 | } 54 | } -------------------------------------------------------------------------------- /tests/DiceCalc/Tests/Calc/BasicRollTest.php: -------------------------------------------------------------------------------- 1 | assertTrue( 20 | is_numeric($result), 21 | sprintf('Calc::calc(%s) result is not numeric: %s', $expression, $result) 22 | ); 23 | 24 | 25 | Random::queue_min(); 26 | $calc = new Calc($expression); 27 | $result = $calc(); 28 | $this->assertTrue( 29 | ($result == $left), 30 | sprintf('Calc::calc(%s) result %s is not not bigger or equal %d', $expression, $result, $left) 31 | ); 32 | 33 | Random::queue_max(); 34 | $calc = new Calc($expression); 35 | $result = $calc(); 36 | $this->assertTrue( 37 | ($result == $right), 38 | sprintf('Calc::calc(%s) result %s is not not less or equal %d', $expression, $result, $right) 39 | ); 40 | 41 | Random::clear_queue(); 42 | } 43 | 44 | public function basicRollProvider() 45 | { 46 | return [ 47 | ['d6', 1, 6], 48 | ['1d6', 1, 6], 49 | ['2d6', 2, 12], 50 | ['3d6', 3, 18], 51 | ['100d6', 100, 600], 52 | 53 | ['d20', 1, 20], 54 | ['1d20', 1, 20], 55 | ['2d20', 2, 40], 56 | 57 | ['d%', 1, 100], 58 | ['1d%', 1, 100], 59 | 60 | ['df', -1, 1], 61 | ['1df', -1, 1], 62 | ['2df', -2, 2], 63 | ['6df', -6, 6], 64 | ['100df', -100, 100], 65 | ]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/DiceCalc/Tests/Calc/BasicRollWithMathTest.php: -------------------------------------------------------------------------------- 1 | assertTrue( 21 | is_numeric($result), 22 | sprintf('Calc::calc(%s) result is not numeric: %s', $expression, $result) 23 | ); 24 | $this->assertTrue( 25 | ($result >= $left), 26 | sprintf('Calc::calc(%s) result %s is not not bigger or equal %d', $expression, $result, $left) 27 | ); 28 | $this->assertTrue( 29 | ($result <= $right), 30 | sprintf('Calc::calc(%s) result %s is not not less or equal %d', $expression, $result, $right) 31 | ); 32 | } 33 | } 34 | 35 | public function diceRollProvider() 36 | { 37 | return [ 38 | ['d6+1', 2, 7], 39 | ['1d6+1', 2, 7], 40 | ['2d6+1', 3, 13], 41 | ['3d6+3', 6, 21], 42 | ['100d6+10', 110, 610], 43 | 44 | ['d20+5', 6, 26], 45 | ['1d20+3', 4, 23], 46 | ['2d20+4', 6, 44], 47 | 48 | ['d%+3', 4, 104], 49 | ['1d%+55', 56, 155], 50 | 51 | ['df+3', 2, 4], 52 | ['1df+5', 4, 6], 53 | ['2df+8', -6, 10], 54 | ['6df+1', -5, 7], 55 | ['100df+3', -97, 103], 56 | 57 | ['d6+d3', 2, 9], 58 | ['1d6+1d3', 2, 9], 59 | ['2d6+1d3', 3, 15], 60 | ['1d6+2d3', 3, 12], 61 | ['10d6+5d3', 15, 85], 62 | ]; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/DiceCalc/Tests/Calc/ConditionNumberTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, $result); 19 | } 20 | 21 | public function diceMathConditionProvider() 22 | { 23 | return [ 24 | ['1+1 = 2', true], 25 | ['3+8 = 11', true], 26 | 27 | ['1+1 > 1', true], 28 | ['1+1 > 3', false], 29 | 30 | ['1+1 < 1', false], 31 | ['1+1 < 3', true], 32 | 33 | ['4+4 > 7', true], 34 | ['4+4 > 8', false], 35 | ['4+4 > 9', false], 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/DiceCalc/Tests/Calc/CustomDieTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(is_numeric($result)); 24 | 25 | $i++; 26 | 27 | $this->assertTrue( 28 | in_array($result, $availableValues), 29 | sprintf('Roll result %s not found in expected array', $result) 30 | ); 31 | 32 | if (!array_key_exists($result, $foundElements)) { 33 | $foundElements[$result] = 0; 34 | } 35 | 36 | $foundElements[$result]++; 37 | 38 | if (count($foundElements) == count($availableValues)) { 39 | $allFound = true; 40 | } 41 | 42 | } while ($allFound === false && $i < 1000); 43 | 44 | $this->assertTrue($allFound, sprintf('Not all variants passed for %s', $expression)); 45 | 46 | } 47 | 48 | public function customNumericDieProvider() 49 | { 50 | return [ 51 | [ 52 | 'd[1,2,2,3,3,4]', 53 | [1, 2, 3, 4], 54 | ], 55 | [ 56 | 'd[2,4,6]', 57 | [2,4,6], 58 | ], 59 | ]; 60 | } 61 | 62 | /** 63 | * @dataProvider customColorDieProvider 64 | */ 65 | public function testCustomColorDie($expression, array $availableValues) 66 | { 67 | 68 | $allFound = false; 69 | $i = 0; 70 | $foundElements = []; 71 | 72 | $checkValues = []; 73 | 74 | foreach ($availableValues as $value) { 75 | $checkValues[] = '['.$expression.':'.$value.']'; 76 | } 77 | 78 | do { 79 | $calc = new Calc($expression); 80 | 81 | $result = $calc(); 82 | 83 | $this->assertInstanceOf('DiceCalc\CalcDice', $result); 84 | 85 | $resultString = (string) $result->value(); 86 | 87 | $i++; 88 | 89 | $this->assertTrue( 90 | in_array($resultString, $checkValues), 91 | sprintf('Roll result %s not found in expected array', $result) 92 | ); 93 | 94 | if (!array_key_exists($resultString, $foundElements)) { 95 | $foundElements[$resultString] = 0; 96 | } 97 | 98 | $foundElements[$resultString]++; 99 | 100 | if (count($foundElements) == count($availableValues)) { 101 | $allFound = true; 102 | } 103 | 104 | } while ($allFound === false && $i < 1000); 105 | 106 | $this->assertTrue($allFound, sprintf('Not all variants passed for %s', $expression)); 107 | 108 | } 109 | 110 | public function customColorDieProvider() 111 | { 112 | return [ 113 | [ 114 | 'd[red,green,blue,yellow]', 115 | ['red', 'green', 'blue', 'yellow'], 116 | ], 117 | [ 118 | 'd[red,green,blue]', 119 | ['red', 'green', 'blue'], 120 | ], 121 | ]; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/DiceCalc/Tests/Calc/KeepPostfixTest.php: -------------------------------------------------------------------------------- 1 | assertTrue( 29 | is_numeric($result), 30 | sprintf('Calc::calc(%s) result is not numeric: %s', $expression, $result) 31 | ); 32 | 33 | if ($result > 0) { 34 | $this->assertTrue( 35 | ($result >= $minResult), 36 | sprintf('Calc::calc(%s) result %s is not not bigger or equal %d', $expression, $result, $minResult) 37 | ); 38 | $this->assertTrue( 39 | ($result <= $maxResult), 40 | sprintf('Calc::calc(%s) result %s is not not less or equal %d', $expression, $result, $maxResult) 41 | ); 42 | } 43 | } 44 | } 45 | 46 | public function dataForKeepPostfixGreaterThan() 47 | { 48 | return [ 49 | ['5d6 keep > 4', 5, 4, 6], 50 | ['5d6k>4', 5, 4, 6] 51 | ]; 52 | } 53 | 54 | /** 55 | * @dataProvider dataForKeepPostfixLessThan 56 | * 57 | * @param $expression 58 | * @param $rollsNum 59 | * @param $minExpected 60 | * @param $maxExpected 61 | */ 62 | public function testKeepPostfixLessThan($expression, $rollsNum, $minExpected, $maxExpected) 63 | { 64 | $minResult = $minExpected; 65 | $maxResult = $rollsNum * $maxExpected; 66 | 67 | for ($i = 0; $i < 100; $i++) { 68 | 69 | $calc = new Calc($expression); 70 | 71 | $result = $calc(); 72 | 73 | $this->assertTrue( 74 | is_numeric($result), 75 | sprintf('Calc::calc(%s) result is not numeric: %s', $expression, $result) 76 | ); 77 | 78 | if ($result > 0) { 79 | $this->assertTrue( 80 | ($result >= $minResult), 81 | sprintf('Calc::calc(%s) result %s is not not bigger or equal %d', $expression, $result, $minResult) 82 | ); 83 | $this->assertTrue( 84 | ($result <= $maxResult), 85 | sprintf('Calc::calc(%s) result %s is not not less or equal %d', $expression, $result, $maxResult) 86 | ); 87 | } 88 | } 89 | } 90 | 91 | public function dataForKeepPostfixLessThan() 92 | { 93 | return [ 94 | ['5d6 keep < 4', 5, 1, 3], 95 | ['5d6k<4', 5, 1, 3] 96 | ]; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /tests/DiceCalc/Tests/Calc/LowestHighestTest.php: -------------------------------------------------------------------------------- 1 | assertTrue( 25 | is_numeric($result), 26 | sprintf('Calc::calc(%s) result is not numeric: %s', $expression, $result) 27 | ); 28 | } 29 | } 30 | 31 | public function dataForLowestPostfix() 32 | { 33 | return [ 34 | ['2d20 lowest 2', 2, 2], 35 | ['2d20l2', 2, 2] 36 | ]; 37 | } 38 | 39 | /** 40 | * @dataProvider dataForHighestPostfix 41 | * 42 | * @param $expression 43 | * @param $rollsNum 44 | * @param $maxResults 45 | */ 46 | public function testHighestPostfix($expression, $rollsNum, $maxResults) 47 | { 48 | 49 | for ($i = 0; $i < 100; $i++) { 50 | 51 | $calc = new Calc($expression); 52 | 53 | $result = $calc(); 54 | 55 | $this->assertTrue( 56 | is_numeric($result), 57 | sprintf('Calc::calc(%s) result is not numeric: %s', $expression, $result) 58 | ); 59 | } 60 | } 61 | 62 | public function dataForHighestPostfix() 63 | { 64 | return [ 65 | ['4d6 highest 3', 4, 1, 3], 66 | ['4d6h3', 4, 1, 3] 67 | ]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/DiceCalc/Tests/Calc/MathTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, $result); 18 | } 19 | 20 | public function diceMathProvider() 21 | { 22 | return [ 23 | ['0+0', 0], 24 | ['1+1', 2], 25 | ['1+0', 1], 26 | ['0+1', 1], 27 | ['10+1', 11], 28 | ['1*1', 1], 29 | ['1*2', 2], 30 | ['2*1', 2], 31 | ['4*5', 20], 32 | ['4+4*5', 24], 33 | ['(4+4)*5', 40], 34 | ['(100+9)*40', 4360], 35 | 36 | ['1-1', 0], 37 | ['1-10', -9], 38 | ['10-1', 9], 39 | ['4-4*5', -16], 40 | ['(4-4)*5', 0], 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/DiceCalc/Tests/Calc/TargetNumberTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($result, sprintf('We never reached true from %s', $expression)); 26 | } 27 | 28 | public function diceRollTargetProvider() 29 | { 30 | return [ 31 | ['d6 > 1'], 32 | ['d6 > 3'], 33 | ['d6 > 5'], 34 | 35 | ['d6 < 2'], 36 | ['d6 < 4'], 37 | ['d6 < 6'], 38 | 39 | ['2d6 > 2'], 40 | ['2d6 < 12'], 41 | 42 | ['2d6+5 > 7'], 43 | ['2d6+5 < 17'], 44 | 45 | ['2d6-1 > 1'], 46 | ['2d6-5 < 6'], 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/DiceCalc/Tests/CalcOperation/AddTest.php: -------------------------------------------------------------------------------- 1 | assertSame($expected, $actual); 21 | } 22 | 23 | public function dataForAdd() 24 | { 25 | return [ 26 | [5, 2, 7], 27 | ['5', 2, 7], 28 | [5, '2', 7], 29 | ['5', '2', 7], 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/DiceCalc/Tests/CalcOperation/CalcTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, $actual); 22 | } 23 | 24 | /** 25 | * @expectedException \Exception 26 | */ 27 | public function testCalcUnknownOperator() { 28 | CalcOperation::calc('?', 1, 2); 29 | } 30 | 31 | public function dataForCalc() 32 | { 33 | return [ 34 | ['+', 5, 2, 7], 35 | ['*', 5, 2, 10], 36 | ['-', 5, 2, 3], 37 | ['/', 5, 2, 2.5], 38 | ['^', 5, 2, 25], 39 | ['>', 5, 2, true], 40 | ['<', 5, 2, false], 41 | ['<', 2, 5, true], 42 | ['>', 2, 5, false], 43 | ['=', 2, 2, true], 44 | ['=', 2, 3, false], 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/DiceCalc/Tests/CalcOperation/DevideTest.php: -------------------------------------------------------------------------------- 1 | assertSame($expected, $actual); 21 | } 22 | 23 | public function dataForDivide() 24 | { 25 | return [ 26 | [5, 2, 2.5], 27 | ['5', 2, 2.5], 28 | [5, '2', 2.5], 29 | ['5', '2', 2.5], 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/DiceCalc/Tests/CalcOperation/EqualToTest.php: -------------------------------------------------------------------------------- 1 | assertSame($expected, $actual); 21 | } 22 | 23 | public function dataForEqualTo() 24 | { 25 | return [ 26 | [1, 1, true], 27 | ['1', 1, true], 28 | [1, '1', true], 29 | ['1', '1', true], 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/DiceCalc/Tests/CalcOperation/ExponentTest.php: -------------------------------------------------------------------------------- 1 | assertSame($expected, $actual); 21 | } 22 | 23 | public function dataForExponent() 24 | { 25 | return [ 26 | [2, 8, 256], 27 | ['2', 8, 256], 28 | [2, '8', 256], 29 | ['2', '8', 256], 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/DiceCalc/Tests/CalcOperation/GreaterThanTest.php: -------------------------------------------------------------------------------- 1 | ', $num1, $num2); 19 | 20 | $this->assertSame($expected, $actual); 21 | } 22 | 23 | public function dataForGreaterThan() 24 | { 25 | return [ 26 | [2, 1, true], 27 | ['2', 1, true], 28 | [2, '1', true], 29 | ['2', '1', true], 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/DiceCalc/Tests/CalcOperation/LessThanTest.php: -------------------------------------------------------------------------------- 1 | assertSame($expected, $actual); 21 | } 22 | 23 | public function dataForLessThan() 24 | { 25 | return [ 26 | [1, 2, true], 27 | ['1', 2, true], 28 | [1, '2', true], 29 | ['1', '2', true], 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/DiceCalc/Tests/CalcOperation/MultiplyTest.php: -------------------------------------------------------------------------------- 1 | assertSame($expected, $actual); 21 | } 22 | 23 | public function dataForMultiply() 24 | { 25 | return [ 26 | [5, 2, 10], 27 | ['5', 2, 10], 28 | [5, '2', 10], 29 | ['5', '2', 10], 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/DiceCalc/Tests/CalcOperation/NaNTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(3, $actual); 15 | } 16 | 17 | /** 18 | * @throws \Exception 19 | * @expectedException \Exception 20 | */ 21 | public function testReduceOnThrowingException() 22 | { 23 | $param = new \stdClass(); 24 | 25 | CalcOperation::reduce($param); 26 | } 27 | 28 | public function testReduceOnCalc() 29 | { 30 | $param = new Calc('1d6'); 31 | 32 | $actual = CalcOperation::reduce($param); 33 | 34 | $this->assertTrue(($actual >= 1)); 35 | $this->assertTrue(($actual <= 6)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/DiceCalc/Tests/CalcOperation/SubtractTest.php: -------------------------------------------------------------------------------- 1 | assertSame($expected, $actual); 21 | } 22 | 23 | public function dataForSubtract() 24 | { 25 | return [ 26 | [5, 2, 3], 27 | ['5', 2, 3], 28 | [5, '2', 3], 29 | ['5', '2', 3], 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | add('DiceCalc\\Tests\\', __DIR__ . '/../../'); 19 | unset($classLoader); 20 | --------------------------------------------------------------------------------