Source code for d20.expression

import abc
import random

from . import diceast as ast, errors

__all__ = (
    "Number",
    "Expression",
    "Literal",
    "UnOp",
    "BinOp",
    "Parenthetical",
    "Set",
    "Dice",
    "Die",
    "SetOperator",
    "SetSelector",
)


# ===== ast -> expression models =====
[docs]class Number(abc.ABC, ast.ChildMixin): # num """ The base class for all expression objects. Note that Numbers implement all the methods of a :class:`~d20.ast.ChildMixin`. """ __slots__ = ("kept", "annotation") def __init__(self, kept=True, annotation=None): self.kept = kept self.annotation = annotation @property def number(self): """ Returns the numerical value of this object. :rtype: int or float """ return sum(n.number for n in self.keptset) @property def total(self): """ Returns the numerical value of this object with respect to whether it's kept. Generally, this is preferred to use over ``number``, as this will return 0 if the number node was dropped. :rtype: int or float """ return self.number if self.kept else 0 @property def set(self): """ Returns the set representation of this object. :rtype: list[Number] """ raise NotImplementedError @property def keptset(self): """ Returns the set representation of this object, but only including children whose values were not dropped. :rtype: list[Number] """ return [n for n in self.set if n.kept]
[docs] def drop(self): """ Makes the value of this Number node not count towards a total. """ self.kept = False
def __int__(self): return int(self.total) def __float__(self): return float(self.total) def __repr__(self): return f"<Number total={self.total} kept={self.kept}>" # overridden methods for typechecking
[docs] def set_child(self, index, value): """ Sets the ith child of this Number. :param int index: Which child to set. :param value: The Number to set it to. :type value: Number """ super().set_child(index, value)
@property def children(self): """:rtype: list[Number]""" raise NotImplementedError
[docs]class Expression(Number): """Expressions are usually the root of all Number trees.""" __slots__ = ("roll", "comment") def __init__(self, roll, comment, **kwargs): """ :type roll: Number """ super().__init__(**kwargs) self.roll = roll self.comment = comment @property def number(self): return self.roll.number @property def set(self): return self.roll.set @property def children(self): return [self.roll] def set_child(self, index, value): self._child_set_check(index) self.roll = value def __repr__(self): return f"<Expression roll={self.roll} comment={self.comment}>"
[docs]class Literal(Number): """A literal integer or float.""" __slots__ = ("values", "exploded") def __init__(self, value, **kwargs): """ :type value: int or float """ super().__init__(**kwargs) self.values = [value] # history is tracked to support mi/ma op self.exploded = False @property def number(self): return self.values[-1] @property def set(self): return [self] @property def children(self): return []
[docs] def explode(self): self.exploded = True
[docs] def update(self, value): """ :type value: int or float """ self.values.append(value)
def __repr__(self): return f"<Literal {self.number}>"
[docs]class UnOp(Number): """Represents a unary operation.""" __slots__ = ("op", "value") UNARY_OPS = {"-": lambda v: -v, "+": lambda v: +v} def __init__(self, op, value, **kwargs): """ :type op: str :type value: Number """ super().__init__(**kwargs) self.op = op self.value = value @property def number(self): return self.UNARY_OPS[self.op](self.value.total) @property def set(self): return [self] @property def children(self): return [self.value] def set_child(self, index, value): self._child_set_check(index) self.value = value def __repr__(self): return f"<UnOp op={self.op} value={self.value}>"
[docs]class BinOp(Number): """Represents a binary operation.""" __slots__ = ("op", "left", "right") BINARY_OPS = { "+": lambda l, r: l + r, "-": lambda l, r: l - r, "*": lambda l, r: l * r, "/": lambda l, r: l / r, "//": lambda l, r: l // r, "%": lambda l, r: l % r, "<": lambda l, r: int(l < r), ">": lambda l, r: int(l > r), "==": lambda l, r: int(l == r), ">=": lambda l, r: int(l >= r), "<=": lambda l, r: int(l <= r), "!=": lambda l, r: int(l != r), } def __init__(self, left, op, right, **kwargs): """ :type op: str :type left: Number :type right: Number """ super().__init__(**kwargs) self.op = op self.left = left self.right = right @property def number(self): try: return self.BINARY_OPS[self.op](self.left.total, self.right.total) except ZeroDivisionError: raise errors.RollValueError("Cannot divide by zero.") @property def set(self): return [self] @property def children(self): return [self.left, self.right] def set_child(self, index, value): self._child_set_check(index) if self.children[index] is self.left: self.left = value else: self.right = value def __repr__(self): return f"<BinOp left={self.left} op={self.op} right={self.right}>"
[docs]class Parenthetical(Number): """Represents a value inside parentheses.""" __slots__ = ("value", "operations") def __init__(self, value, operations=None, **kwargs): """ :type value: Number :type operations: list[SetOperator] """ super().__init__(**kwargs) if operations is None: operations = [] self.value = value self.operations = operations @property def total(self): return self.value.total if self.kept else 0 @property def set(self): return self.value.set @property def children(self): return [self.value] def set_child(self, index, value): self._child_set_check(index) self.value = value def __repr__(self): return f"<Parenthetical value={self.value} operations={self.operations}>"
[docs]class Set(Number): """Represents a set of values.""" __slots__ = ("values", "operations") def __init__(self, values, operations=None, **kwargs): """ :type values: list[Number] :type operations: list[SetOperator] """ super().__init__(**kwargs) if operations is None: operations = [] self.values = values self.operations = operations @property def set(self): return self.values @property def children(self): return self.values def set_child(self, index, value): self._child_set_check(index) self.values[index] = value def __repr__(self): return f"<Set values={self.values} operations={self.operations}>" def __copy__(self): return Set(values=self.values.copy(), operations=self.operations.copy())
[docs]class Dice(Set): """A set of Die.""" __slots__ = ("num", "size", "_context") def __init__(self, num, size, values, operations=None, context=None, **kwargs): """ :type num: int :type size: int|str :type values: list of Die :type operations: list[SetOperator] :type context: dice.RollContext """ super().__init__(values, operations, **kwargs) self.num = num self.size = size self._context = context @classmethod def new(cls, num, size, context=None): return cls(num, size, [Die.new(size, context=context) for _ in range(num)], context=context)
[docs] def roll_another(self): self.values.append(Die.new(self.size, context=self._context))
@property def children(self): return [] def __repr__(self): return f"<Dice num={self.num} size={self.size} values={self.values} operations={self.operations}>" def __copy__(self): return Dice( num=self.num, size=self.size, context=self._context, values=self.values.copy(), operations=self.operations.copy(), )
[docs]class Die(Number): # part of diceexpr """Represents a single die.""" __slots__ = ("size", "values", "_context") def __init__(self, size, values, context=None): """ :type size: int :type values: list of Literal :type context: dice.RollContext """ super().__init__() self.size = size self.values = values self._context = context @classmethod def new(cls, size, context=None): inst = cls(size, [], context=context) inst._add_roll() return inst @property def number(self): return self.values[-1].total @property def set(self): return [self.values[-1]] @property def children(self): return [] def _add_roll(self): if self.size != "%" and self.size < 1: raise errors.RollValueError("Cannot roll a 0-sided die.") if self._context: self._context.count_roll() if self.size == "%": n = Literal(random.randrange(10) * 10) else: n = Literal(random.randrange(self.size) + 1) # 200ns faster than randint(1, self._size) self.values.append(n) def reroll(self): if self.values: self.values[-1].drop() self._add_roll() def explode(self): if self.values: self.values[-1].explode() # another Die is added by the explode operator def force_value(self, new_value): if self.values: self.values[-1].update(new_value) def __repr__(self): return f"<Die size={self.size} values={self.values}>"
# noinspection PyUnresolvedReferences # selecting on Dice will always return Die
[docs]class SetOperator: # set_op, dice_op """Represents an operation on a set.""" __slots__ = ("op", "sels") def __init__(self, op, sels): """ :type op: str :type sels: list[SetSelector] """ self.op = op self.sels = sels @classmethod def from_ast(cls, node): return cls(node.op, [SetSelector.from_ast(n) for n in node.sels])
[docs] def select(self, target, max_targets=None): """ Selects the operands in a target set. :param target: The source of the operands. :type target: Number :param max_targets: The maximum number of targets to select. :type max_targets: Optional[int] """ out = set() for selector in self.sels: batch_max = None if max_targets is not None: batch_max = max_targets - len(out) if batch_max == 0: break out.update(selector.select(target, max_targets=batch_max)) return out
[docs] def operate(self, target): """ Operates in place on the values in a target set. :param target: The source of the operands. :type target: Number """ operations = { "k": self.keep, "p": self.drop, # dice only "rr": self.reroll, "ro": self.reroll_once, "ra": self.explode_once, "e": self.explode, "mi": self.minimum, "ma": self.maximum, } operations[self.op](target)
def keep(self, target): """ :type target: Set """ for value in target.keptset: if value not in self.select(target): value.drop() def drop(self, target): """ :type target: Set """ for value in self.select(target): value.drop() def reroll(self, target): """ :type target: Dice """ to_reroll = self.select(target) while to_reroll: for die in to_reroll: die.reroll() to_reroll = self.select(target) def reroll_once(self, target): """ :type target: Dice """ for die in self.select(target): die.reroll() def explode(self, target): """ :type target: Dice """ to_explode = self.select(target) already_exploded = set() while to_explode: for die in to_explode: die.explode() target.roll_another() already_exploded.update(to_explode) to_explode = self.select(target).difference(already_exploded) def explode_once(self, target): """ :type target: Dice """ for die in self.select(target, max_targets=1): die.explode() target.roll_another() def minimum(self, target): # immediate """ :type target: Dice """ selector = self.sels[-1] if selector.cat is not None: raise errors.RollValueError(f"{str(selector)} is not a valid selector for minimums.") the_min = selector.num for die in target.keptset: if die.number < the_min: die.force_value(the_min) def maximum(self, target): # immediate """ :type target: Dice """ selector = self.sels[-1] if selector.cat is not None: raise errors.RollValueError(f"{str(selector)} is not a valid selector for maximums.") the_max = selector.num for die in target.keptset: if die.number > the_max: die.force_value(the_max) def __str__(self): return "".join([f"{self.op}{str(sel)}" for sel in self.sels]) def __repr__(self): return f"<SetOperator op={self.op} sels={self.sels}>"
[docs]class SetSelector: # selector """Represents a selection on a set.""" __slots__ = ("cat", "num") def __init__(self, cat, num): """ :type cat: str or None :type num: int """ self.cat = cat self.num = num @classmethod def from_ast(cls, node): return cls(node.cat, node.num)
[docs] def select(self, target, max_targets=None): """ Selects operands from a target set. :param target: The source of the operands. :type target: Number :param int max_targets: The maximum number of targets to select. :return: The targets in the set. :rtype: set of Number """ selectors = {"l": self.lowestn, "h": self.highestn, "<": self.lessthan, ">": self.morethan, None: self.literal} selected = selectors[self.cat](target) if max_targets is not None: selected = selected[:max_targets] return set(selected)
def lowestn(self, target): return sorted(target.keptset, key=lambda n: n.total)[: self.num] def highestn(self, target): return sorted(target.keptset, key=lambda n: n.total, reverse=True)[: self.num] def lessthan(self, target): return [n for n in target.keptset if n.total < self.num] def morethan(self, target): return [n for n in target.keptset if n.total > self.num] def literal(self, target): return [n for n in target.keptset if n.total == self.num] def __str__(self): if self.cat: return f"{self.cat}{self.num}" return str(self.num) def __repr__(self): return f"<SetSelector cat={self.cat} num={self.num}>"