def evalpostfix(postfixexpr): nums = Stack() tokenlist = list(postfixexpr.replace(' ', '')) for token in tokenlist: if token not in prec.keys(): nums.push(token) else: right = nums.pop() left = nums.pop() expression = left + token + right result = eval(expression) nums.push(str(result)) if nums.size() == 1: return nums.pop()
class DiceResolver: def __init__(self): # BEDMAS ==> d()!^/%*+- # PEMDAS ==> d()!^*/%+- # Precedence weighting reflects rule; higher means priority # Close bracket ')' not included; it is a special case (collapses stack to '(') self.precedence = { "(": 0, "+": 3, "-": 3, "/": 5, "*": 5, "%": 5, "C": 5, "c": 5, "^": 7, "!": 8, "d": 9 } # 's' is a stack used for rearranging infix arguments self.s = Stack() # 'q' is a queue used to store the postfix arguments self.q = Queue() self.error = False # Converts a valid infix expression (mathematical expression) # to postfix using Reverse Polish Notation (RPN). Infix exp- # ression must be valid; this function can not check validi- # ty. Note that by design, this only supports integer expr- # ession (no floating point support). FP support can be add- # ed if while building numbers, the '.' character is accepted. # Example: Expression="1 + 2 * 3" --> 7, NOT 9 # RPN="1 2 3 * +" --> 7 # Note that the order of operations is preserved in the RPN. def infixToRPN(self, expression): # Since a number may be multiple characters, we start with an empty string, # and while each character is numeric, we append the number until a non- # numeric value is encountered. num = "" # Tokenize expression character by character for c in expression: token = str(c) # Case: we had accumulated a number but this character is not a # numeric value; so save accumulated number, and reset accumulator. if (num != "" and not token.isnumeric()): self.q.enqueue(num) num = "" # We aren't a number; so handle the token # '(' start brackets are simply markers of what point to return to when # a ')' close bracket is encountered. if (token == "("): self.s.push(token) # Special case; we look for this first -> it means we have to pop all # previous values off stack into the RPN queue until we find the '(' elif (token == ")"): # pop up until the bracket while (self.s.peek() != "("): self.q.enqueue(self.s.pop()) # pop the bracket / throw it away (it was just a marker, we're done with it) self.s.pop() # Casee: operator handling # we are done handling brackets, check for a valid operator elif (token in self.precedence): while self.s.size() != 0 and (self.precedence[token] <= self.precedence[self.s.peek()]): self.q.enqueue(self.s.pop()) self.s.push(token) # Case: character is numeric. # Append to accumulator and continue parsing elif (token.isnumeric()): num += token # Did token end on a number? If so store accumulated number in RPN queue if (num != ""): self.q.enqueue(num) # Now pop items from stack to the queue to cleanup while (self.s.size() != 0): self.q.enqueue(self.s.pop()) # At this point, we have a valid RPN in the 'q' queue # (if the infix expression was valid) # Let's return a string version: q_cp = self.q.copy() rpn = "" for c in q_cp: rpn += c + " " return (rpn) # Routine to calculate a factorial def factorial(self, value): if (value < 0): return (0) elif (value == 0 or value == 1): return (1) elif (value == 2): return (2) product = value for x in range(2, value): product = product * x return (product) # Routine to calculate "choose" (combinatorics) # Formula: # nCr (n Choose r) = n! / r!(n-r)! def choose(self, n, r): numerator = self.factorial(n) denominator = self.factorial(r) * self.factorial(n - r) # Sanity if (denominator == 0): return (0) # Compute # NOTE: Should always be an integer result, but cast # it anyways to be safe return (int(numerator / denominator)) # Given left value, right value, and an operator, calculate. def calculate(self, left, right, op): if (op == "+"): return (left + right) elif (op == "-"): return (left - right) elif (op == "*"): return (left * right) elif (op == "/"): return (int(left / right)) elif (op == "^"): return (left**right) elif (op == "%"): return (left % right) elif (op == "!"): return (self.factorial(left)) elif (op == "c" or op == "C"): return (self.choose(left, right)) # dice roll; handled with 'random' # NOTE: expressions without 'd' are deterministic; # expressions with 'd' are non-deterministic (variable # outcomes). elif (op == "d"): sum = 0 # Left value is number of rolls; right value is die # IE 3d6 = 3 rolls of a 6 sided die, summed. for i in range(left): sum += random.randint(1, right) return (sum) # whoops shouldn't have happened try to be graceful return (0) # Nifty little stack and queue algorithm for evaluating # the RPN. Expects a valid RPN expression. def evaluateRPN(self): workstack = Stack() # As we pull tokens from the queue, we validate them and if neither a number # nor an operator, we abort with an error. for t in self.q.copy(): if (t in self.precedence): # As we work backwards, right value is first; validate right = workstack.pop() if (not str(right).isnumeric() and not right in self.precedence): self.error = True break # Now get left value, validate # Special case: ! only takes one argument. Make them identical if (t == "!"): left = right else: left = workstack.pop() if (not str(left).isnumeric() and not left in self.precedence): self.error = True break # Both valid, so calculate workstack.push(self.calculate(left, right, t)) else: workstack.push(int(t)) # answer is now on the stack if (not self.error): return (workstack.pop()) else: return (0) # One function to handle it all. How Pythonic. def resolve(self, expression, repeat=False): if not repeat: self.error = False self.q.clear() self.s.clear() self.infixToRPN(expression) return (self.evaluateRPN()) else: # Repeat=True # This allows repeat dice rolls / calculations, without rebuilding # the RPN queue each time. return (self.evaluateRPN()) # Heuristic to calculate expression distribution, ment to be # used with dice rolls (ie, 2d6). This is done by repeating rolls # to a cap of n trials, then assessing the results. # Returns a histogram report with trial results, mean and mode. def getHistogram(self, expression, trials): # Validate min/max boundaries if (trials < 0): trials = 1 elif (trials > 1000000): trials = 1000000 # Initialize sb = "" rolls = dict() sum = 0 pct = 1.0 max = dict() result = dict() # Build for i in range(trials): if i == 0: roll = self.resolve(expression) else: # We already built the RPN, don't waste cycles # reuilding it on every iteration, set repeat=True. roll = self.resolve(expression, repeat=True) # Track the recurrences of roll values if (roll in rolls.keys()): rolls[roll] += 1 else: rolls[roll] = 1 # Nifty way to build a key sorted report keys = list(rolls.keys()) keys.sort() # Report sb = f"DISTRIBUTION HISTORGRAM ({trials:,} trials):\n" max[0] = max[1] = 0 for key in keys: result[key] = rolls[key] sum += key * rolls[key] pct = float(rolls[key]) / float(trials, ) * 100.0 if (pct > max[0]): max[0] = pct max[1] = key sb += f"[{key:3}] ==> {rolls[key]:,} ({pct:.2f}%)\n" # Stash pct for later rolls[key] = pct mean = float(sum) / float(trials) + 0.5 sb += f"Mean: {mean:.2f}\n" mode = max[1] sb += f"Mode: {mode}\n\n" # Scaling calculation uses a lambda function scale = lambda x, y: int(float(x / 100) * float(y) + 0.5) # Build histogram pictogragh pic = "PICTORIAL HISTOGRAM:\n" for key in keys: pic += f"[{key:3}] " for i in range(scale(rolls[key], 160)): pic += "*" pic += "\n" # Send back the report return (sb + pic)